1.内存和地址
1.1内存
内存和地址类似于房间号,例如再哪个区哪栋楼房间号,而内存也是如此,在内存中也被分为一个个小的内存单元,类比房间号,可以很快地找到内存地址,再内存中分为bit->byte->kb->mb->gb->tb->pb,这是内存中cpu的储存方式的划分,类似于一个宿舍住八个人,一层楼住很多宿舍,而一栋楼分为多层,一个区分为多栋楼。通过地址我们可以很快地寻找到目标
1.2究竟该如何理解编址
理解内存和cpu,才能理解编址,cpu和内存是如何链接的?通过地址总线,数据总线和控制总线进行连接,在cpu上想找到某些数据,可以从cpu上输入该数据所在的地址,通过地址总线向内存中找到该数据所在的地址,然后再从内存通过数据总线把数据传输到cpu寄存器进行一些数据操作;先理解一个概念:32/64位机器是什么?可以简单理解为在内存中的每一个位,每一个位有两种状态 0/1 ,可以理解为电脉冲的有无,这个概念有助于理解后面的指针变量的大小;所以什么是编址?编址就是编辑的地址,是内存总自带的内存单元,是本身的地址,并不是每一个内存单元的地址都被存储,而是找到希望找到哪一个地址,哪一个地址被找到
2.指针变量和地址
从上面可以简单理解为指针就是地址,而指针变量就是存储地址的变量,以下是一段简单的指针代码理解
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
*pa = 20;
return 0;
}
2.1取地址操作符(&)
在上文中可以看到 &a 这个操作,那是什么意思? 从头解释:开辟4个字节空间,给a变量赋值10,创建一个数据类型为 int *类型的变量,int *就是pa的数据类型,是int类的指针变量,&a的意思是取出a的地址放在pa这个指针变量里,那么可以把指针变量pa的类型改为char*吗?可以,但是会报错,因为要存的a是int类,如果想改成char*类型,可以进行强制类型转换
#include <stdio.h>
int main()
{
int a = 20;
char* pa = &a;
*pa = 20;
printf("%d", a);
return 0;
}
下列是进行强制类型转化之后就没有该警告了
#include <stdio.h>
int main()
{
int a=20;
char*pa=(char)&a;//这段代码无警告
return 0;
}
2.2 指针变量和解引⽤操作符(*)
#include <stdio.h>
int main()
{
int a = 20;
char* pa = &a;
*pa = 20;// * 操作符是拿出pa里的地址,指向地址所指向的空间,改变该空间里的内容
printf("%d", a);
return 0;
}
*操作符作用是把地址进行指向所在的空间,再通过该空间进行改变变量的值
2.2.1 指针变量
指针变量相当于基本数据类型后面加上*,例如int * ,char*,double *
2.2.2 如何拆解指针类型
指针类型,如int * ,char*,double *,把int *,拆成int和*
2.2.3 解引⽤操作符
&:解引用操作符,相当于在指针变量中存放地址,&操作符相当于把通过地址对所指向的空间的内容进行修改
2.3 指针变量的⼤⼩
指针变量的大小是不是固定的?int*,char*,double*,大小是不是一样的?
#include <stdio.h>
int main()
{
int a = 20;
char b;
int* pa = &a;
char* pb = &b;//pa和pb的大小是不是一致的?
printf("%d\n",sizeof(int *);
printf("%d\n",sizeof(char *);
return 0;
}
通过sizeof操作符可以知道int和char数据类型所占内存空间大小都为八字节,这是因为是64位机器的缘故,可以在上面将64改成x86环境,也就是32位环境,
x86是机器字长位32位,相当于32根线,有2^32次方种状态,也就可以存储相对应的内存大小,指针变量大小为4个字节;64则是有64根地址线,有2^64种状态,相对应的指针变量的大小为8字节
3.指针变量类型的意义
3.1 指针的解引⽤
既然指针变量的大小是一样的,那为什么还要分成各种指针变量,char*,int*呢?
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
在调试期间打开监视
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;//强制类型转换可能会出现问题
*pc = 0;
return 0;
}
这两段代码想展示的问题是给n的值进行改变,char会改变一个字节内容,而int会改变四个字节内容,而代码没显示这个问题
3.2 指针+-整数
指针+-整数应该是什么?指针!
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
printf("%p\n", pa);//%p为打印地址
printf("%p", pa+1);
return 0;
}
上述代码和图片可以看出指针+-整数后还是地址,存放在指针变量中
3.3 void* 指针
void*指针变量是可以接收任何地址的指针变量,例如在函数中可以接受各种类型的地址!
#include <stdio.h>
int main()
{
int a=20;
char *pa=(char)&a;//如果这样写编译器就会报错,这时加上强制类型转化就不会出现问题
//如果是void*就可以接受任何类型地址,不用进行强制类型转换,但也有一定的缺点
//例如通过void *类型的指针变量无法进行指针+-整数的操作,也无法进行*操作
return 0;
}
4.const修饰指针
4.1 const修饰变量
const修饰变量,修饰完后该变量不能被改变
#include <stdio.h>
int main()
{
const int a=20;
a=10;//err
return 0;
}
4.2 const修饰指针变量
虽然const修饰的变量不能被表面修改,但可以从别的方式进行改变
#include <stdio.h>
int main()
{
const int a=20;
int *pa=&a;
*pa=20;//通过地址的方式将a的值进行改变
return 0;
}
这里我们就要思考一下,如何让*pa不能改变a的值呢?可以采用以下const修饰来阻止这种修改
#include <stdio.h>
int main()
{
const int a=20;
const int *pa=&a;//const在*左边限制的是*
&pa=0;
return 0;
}
const在左边修饰的是pa,如果const在右边则修饰的是pa
#include <stdio.h>
int main()
{
const int a = 20;
int*const pa = &a;
*pa = 30;
pa=20;//err 这代表把pa所在的空间改变会受到限制
return 0;
}
该图所代表意思为pa存放a的地址,若const修饰的是pa,则代表pa的空间内容受到限制,无法通过后续进行改变
5. 指针运算
指针运算分为三种:指针+-整数,指针-指针,指针的关系运算
5.1 指针+- 整数
指针+-整数在前面已经大概描述过,指针+-整数还是指针
数组中:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* pa = arr[0];
printf("%d ", *pa + 1);//arr[1],会出现1的结果
return 0;
}
5.2 指针-指针
指针-指针是整数,由5.1可知指针+-整数还是指针,那么同理,指针-指针一定是整数,可以在数组中进行理解
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* pa = arr[0];
int *pb=&arr[1];
printf("%d ", pa+pb);
return 0;
}
由此图可知,指针减指针是整数,也是数组之间的元素个数,由此,我们可以写出strlen库函数的定义
#include <stdio.h>
#include <assert.h>
int my_strlen(const char* p)//这里可以进行优化,使用const修饰
{
int count = 0;
assert(p);//在循环外使用assert断言,使p这个指针变量
while (*p)
{
if (*p == '\0')
{
break;
}
p++;
count++;
}
return count;
}
int main()
{
int len = my_strlen("abcdefgh");
printf("%d ", len);
return 0;
}
5.3 指针的关系运算
可以通过指针进行数组的遍历
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);//为什么sizeof里放的arr的意思和后面注释意思不同,是否造成了矛盾?
//后文将详细描述..
int* pa = &arr[0];
while (pa<arr+sz)//arr是数组首元素地址,arr+sz就是数组最后一个元素的地址
{
printf("%d ", *pa);
pa++;//pa++代表将arr中元素下标向后移动一位
}
return 0;
}
6. 野指针
野指针定义:指针指向的对象是不明确的,没有对象,指向随机的地址
6.1 野指针成因
主要有三部分可以造成野指针:
1.指针没有进行初始化:
#include <stdio.h>
int main()
{
int *pa;
return 0;
}
2.指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* pa=&arr[0];
for (int i = 0; i < 11; i++)
{
*pa = 1;
pa++;
}
return 0;
}
这里会导致指针越界访问,编译器会出现警告,这也是常见野指针出现的原因
3.指针所指向的空间已经被销毁
int* test()
{
int n = 100;
return &n;
}
#include <stdio.h>
int main()
{
int *ret = test();
printf("%p\n", *ret);//此时的n的地址在调用函数结束后空间已经被销毁,再被销毁后的空间进行打印会出现野指针
return 0;
}
6.2 如何规避野指针
6.2.1 指针初始化
指针初始化
#include <stdio.h>
int main()
{
int a=10;
int *pa=&a;
return 0;
}
如果不知道应该指向哪个对象,可以给个NULL对象
#include <stdio.h>
int main()
{
int *pa=NULL;
return 0;
}
6.2.2 ⼩⼼指针越界
指针越界只能依靠自己或者编译器才能及时看出错误,平时在写指针时要仔细谨慎
6.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
#include <stdio.h>
int main()
{
int a = 20;
//.........
int* pa = &a;
int* pa = NULL;
if (pa != NULL)//判断
{
//......
}
return 0;
}
6.2.4 避免返回局部变量的地址
在上述第三个例子看到如果返回局部变量的地址,就会造成野指针,避免错误
7.assert断⾔
使用assert断言需引用头文件,断言告诉你错在错误的地址,如果返回值是0,则就会报错
#include <stdio.h>
#include <assert.h>
int main()
{
int a = 10;
int* pa = NULL;
assert(pa != NULL);//断言为错误(0)就会报错
return 0;
}
assert只能在debug版本使用,在release会被优化;如何一条语句直接取消所有assert作用?
#define NDEBUG//只需在assert.h头文件中引用#define NDEBUG就会使所有assert失效,以后如果想再使用assert把NDEBUG注释掉
#include <stdio.h>
#include <assert.h>
int main()
{
int a = 10;
int* pa = NULL;
assert(pa != NULL);
return 0;
}
assert优点已说明,也有一定的缺点,会占用内存判断时间
8.指针的使⽤和传址调⽤
8.1 strlen的模拟实现
复习前面所说的模拟strlen实现:字符串进行传参,传到函数中,在循环中,字符串地址向后移动,如果为\0则停止,循环中的计数器停止计数!
#include <stdio.h>
#include <assert.h>
int my_strlen(const char* p)//这里可以进行优化,使用const修饰
{
int count = 0;
assert(p);//在循环外使用assert断言,使p这个指针变量
while (*p)
{
if (*p == '\0')
{
break;
}
p++;
count++;
}
return count;
}
int main()
{
int len = my_strlen("abcdefgh");
printf("%d ", len);
return 0;
}
8.2 传值调⽤和传址调⽤
传值调用很好理解,在函数中传值,把实参传给形参
#include <stdio.h>
int test(int x, int y)
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
int len = test(a, b);
return 0;
}
传址调用:传送地址,例如使用函数进行交换数据,如果不使用函数有多种方式可以进行操作
使用函数:思路:如果只是单纯的把值传过去,则无法把两个值返回,所以把地址传送过去,再函数里对地址进行引用修改
#include <stdio.h>
void* swap(int* x,int* y)//在这里我犯了一个错误,const修饰,如果const修饰,下面赋值操作就会发生错误
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 10;
int b = 20;
swap(&a, &b);
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
以上就是指针的基础部分!