13 学习总结:指针 · 其一

发布于:2024-07-07 ⋅ 阅读:(28) ⋅ 点赞:(0)

目录

一、内存和地址

(一)内存

(二)内存单元

(三)地址

(四)拓展:CPU与内存的联系

二、指针变量和地址

(一)创建变量的本质

(二)取地址操作符:&

(三)指针变量和解引用操作符:*

1、指针变量

2、指针变量的理解

(1)【int* pa】的理解

(2)【int*】的理解

3、解引用操作符:*

(四)指针变量的大小

三、指针变量类型的意义

(一)解引用操作时,决定可以操作多少个字节

(二)指针 + -  整数时,向前/向后走多大的区别

(三)void* 指针

四、const修饰指针

(一)const修饰变量

(二)const修饰指针变量

五、指针的运算

(一)指针 + 或 -  整数

(二)指针 - 指针

(三)指针的关系运算(指针的比较)

六、野指针

(一)野指针造成的原因

1、指针未初始化

2、指针越界访问

3、指针指向的空间释放

(二)如何规避野指针

1、指针初始化

2、小心指针越界

3、指针变量不再使用时,及时置NULL,指针使用之前检查有效性

4、避免返回局部变量的地址

七、assert断言

八、指针的使用和传址调用

(一)指针的使用:strlen的模拟实现

(二)传值调用和传址调用


一、内存和地址

(一)内存

        又称内存储器或主存储器,计算机中所有程序的运行都在内存中进行,计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,这样使用内存则需要高效地管理内存空间;

(二)内存单元

        就是把内存划分为一个个的内存单元,每个内存单元的大小取1个字节(8个比特位),每个内存单元都有⼀个编号,有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间;

(三)地址

        在计算机中我们把【内存单元的编号】也称为【地址】,C语言中给【地址】起了新的名字叫:【指针】

        可以理解为:【内存单元的编号 == 地址 == 指针】

(四)拓展:CPU与内存的联系

        有三条总线将CPU与内存连接彼此,交换数据:①地址总线;②数据总线;③控制总线

        交换过程:地址信息通过【地址总线】被下达给内存,在内存上就可以找到相应的数据,将数据通过【数据总线】传入CPU做处理,【控制总线】则负责传递对数据的操作,如读操作、写操作等

二、指针变量和地址

(一)创建变量的本质

        创建变量的本质是在内存中申请空间,例如创建一个 int 变量就是向内存申请4个字节的空间,每个字节都有自己的编号(地址),变量的名字仅仅是给程序员看的,编译器不看名字,编译器是通过地址找内存单元的

(二)取地址操作符:&

         使用:拿到变量的地址

        例如:

int a = 10;

&a;

        &a 就可以拿到变量a的地址,虽然整型变量占用4个字节,我们只要知道了第⼀个字节地址,春藤摸瓜访问到4个字节的数据也是可行的

        注:当一个变量占多个内存单元的时候,总会取出该变量的第一个内存单元(地址较小的那个字节)

(三)指针变量和解引用操作符:*

1、指针变量

        通过取地址操作符(&)拿到的地址是⼀个数值,比如:0x0012ff40,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中,例如:

#include <stdio.h>
int main()
{
    int a = 10;
    int * pa = &a;//取出a的地址并存储到指针变量pa中
 
    return 0;
}

        指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址

2、指针变量的理解

        上面例子的写法中的 int *pa 拆开来理解:

(1)【int* pa】的理解

        ①【int *】是变量pa的类型;

        ② pa是一个变量,用来存放地址(指针)的,所以pa又叫指针变量

(2)【int*】的理解

        ① * 表示pa是指针变量;

        ② int 表示【pa 指针变量中保存的地址】所指向的【变量 a】的类型是int

3、解引用操作符:*

        又称为间接访问操作符,用法:

        如下演示:
int main()
{
    int a = 100;
    int* pa = &a;
    *pa = 0;
    此处*pa == a,相当于对a进行修改

    return 0;
}

        总结:通过【指针变量pa】找到指向的变量a—— *pa(通过pa的值,找到a)

        ① pa —— 指针变量

        ② &pa —— 指针变量pa的地址

        ③ *pa —— pa指向的变量a

(四)指针变量的大小

        【指针变量类型的大小】取决于【地址的大小】,而地址大小由计算机是32位操作系统还是64位操作系统决定

        ① 指针变量是用来存放地址的,一个地址的存放需要多大空间,那么指针变量类型就是多大,所以32位平台总共有32根地址总线,每根线的电信号转化成数字信号后是1或0,那我们把32根地址总线产生的2进制序列作为一个地址,那么一个地址就是32个比特位,就是4个字节;同理,在64位的机器中,一个地址的大小就是8字节

        ② 地址的大小与【指向的原变量的类型大小】无关,就是4字节或者8字节

#include <stdio.h>

//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)

int main()
{
    printf("%zd\n", sizeof(char *));
    printf("%zd\n", sizeof(short *));
    printf("%zd\n", sizeof(int *));
    printf("%zd\n", sizeof(double *));

    return 0;
}

        X86环境输出结果如下:

        X64环境输出结果如下:

        结论:

32位平台下地址是32个bit位,指针变量大小是4个字节

64位平台下地址是64个bit位,指针变量大小是8个字节

指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的

三、指针变量类型的意义

        指针变量的大小和类型无关,只要是指针变量,在同⼀个平台下,大小都是⼀样的,都是4字节或者8字节,为什么还要有各种各样的指针类型呢?

(一)解引用操作时,决定可以操作多少个字节

        如下演示:

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	int* p = &a;
	*p = 0;

	return 0;
}

        变量a的地址与4个字节的值如下:

        经过 *p = 0;的语句后,4个字节的值全部改为0,如下:

        若代码中指针变量的类型改为char*:

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	char* p = &a;
	*p = 0;

	return 0;
}

        变量a的地址与4个字节的值如下:

        经过 *p = 0;的语句后,4个字节的值只有一个字节改为0,如下:

      

        结论:指针的类型决定了,解引用操作时,决定可以操作多少个字节

        比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节

(二)指针 + -  整数时,向前/向后走多大的区别

        如下代码演示:

#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跳过1个字节, int* 类型的指针变量+1跳过了4个字节;

        结论:指针的类型决定了指针向前或者向后走一步有多大(距离)           

 补充:
        int* pa;   
        pa+1——> +1 * sizeof (int)
        pa+n——> +n * sizeof (int)

        char* pa;   
        pa+1——> +1 * sizeof (char)
        pa+n——> +n * sizeof (char)

总结:

        类型* 变量名;

        变量名 + 1 ——> +1 * sizeof(指针指向的变量类型)

(三)void* 指针

        void* ——无具体类型的指针(泛型指针)

        可以接收任何类型的地址,但是正因为他是泛型指针,所以没有特定类型指针的用法,即无法解引用和进行指针的 + - 操作

        作用:⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得⼀个函数来处理多种类型的数据

四、const修饰指针

(一)const修饰变量

        const修饰变量的时候,叫:常变量;

        本质还是变量,只是不能被修改;

        变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量,若不想变量被直接修改,就使用const修饰变量起限制作用

#include <stdio.h>
int main()
{
    int m = 0;
    m = 20;//m是可以修改的
    const int n = 0;
    n = 20;//n是不能被修改的

    return 0;
}

        上述代码中n是不能被修改的,其实n本质是变量(无法在数组长度中使用),只不过被const修饰后,在语法上加了限制,只要我们在代码中对n进行修改,就不符合语法规则,就报错,致使没法直接修改n

        但是可以拿到n的地址,通过指针对它进行修改,但这是在打破语法规则

int main()
{
    const int n = 0;
    printf("n = %d\n", n);
    int*p = &n;
    *p = 20;
    printf("n = %d\n", n);

    return 0;
}

        结果如下:

        这里的初衷是不让变量改变,但是通过指针还是能打破const的限制,接下来就要对这一象限改进,直接对指针变量做const限制

(二)const修饰指针变量

        ⼀般来讲const修饰指针变量,可以放在 * 的左边,也可以放在 * 的右边,意义是不⼀样的

int * p;//没有const修饰
int const * p;//const 放在*的左边做修饰
int * const p;//const 放在*的右边做修饰

        如下代码演示:

        代码一:

int a = 10;
int b = 20;
int const * p = &a;

*p = 200;err
p = &b;√

        代码一分析:

        这个const限制的是 *p,即p指向的变量a不能改变;但是并没有限制p,所以可以修改p所指向的变量;
        放在*的左边,限制的是指针指向的内容,也就是不能通过指针变量来修改它所指的内容;但是指针变量本身可以改变的

        代码二:

int a = 10;
int b = 20;
int * const p = &a;

*p = 200;√
p = &b;err

        代码二分析:

        放在*的右边,限制的是指针变量本身,也就是指针变量本身不可以改变,但可以通过指针变量来修改它所指的内容

        结论:const修饰指针变量的时候

const如果放在 * 的左边,修饰的是【指针指向的内容 *p】,保证指针指向的内容不能通过指针来改变,但是【指针变量本身 p】的内容可变;
const如果放在*的右边,修饰的是【指针变量本身 p】,保证了指针变量的地址指向不能修改,但是【指针指向的内容*p】,可以通过指针改变

五、指针的运算

        指针的基本运算有三种,分别是:

        • 指针 + 或 -  整数

        • 指针 - 指针

        • 指针的关系运算(指针的比较)

(一)指针 + 或 -  整数

        因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺腾摸瓜就能找到后⾯的所有元素

int arr[10] = {1,2,3,4,5,6,7,8,9,10};

#include <stdio.h>
//指针+- 整数
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = &arr[0];
    int sz = sizeof(arr)/sizeof(arr[0]);
    for(int i = 0; i < sz; i++)
    {
        printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
    }

    return 0;
}

        注意:指针运算是指对 p 进行运算,而不是对*p,若对 *p 运算,就是对变量a运算了

        在数组中,指针能够“顺腾摸瓜”的原因是:

        ①指针类型决定了【指针+1】的步长,和指针解引用之后的权限;

        ②数组在内存中的地址是连续的

        错误演示代码:

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    char *p = &arr[0];
    int sz = sizeof(arr)/sizeof(arr[0]);
    for(int i = 0; i < sz; i++)
    {
        printf("%d ", *p);
        p += 4;
    }

    return 0;
}

        代码分析:

        每次打印时,都让p += 4,在打印1~10时恰好正确,

        每次访问都只会访问第一个字节,后面三个字节是直接跳过的,所以两位数的时候是正确的,但是数字大一些就会忽略掉第二个字节的数字,就会出错

(二)指针 - 指针

        【指针 - 指针】的运算前提条件两个指针指向的是同一个空间,否则运算无意义;

        指针 - 指针的【绝对值】,是指针和指针之间【元素的个数】

        应用:求字符串长度 ,如下代码演示:

#include <stdio.h>

int my_strlen(char *s)
{
    char *p = s;//设置尾指针
    while(*p != '\0' )
        p++;

    return p-s;
}

int main()
{
    printf("%d\n", my_strlen("abc"));
    return 0;
}

        拓展:指针 + 指针?

        答:无意义,类似于 【日期 +- 天数(计算日期)】、【日期 - 日期(算的是两个日期之间差多少天)】有意义,而【日期 + 日期】无意义

(三)指针的关系运算(指针的比较)

        应用:做判断条件使用,数组中,若一个地址小于另一个地址,则执行语句

#include <stdio.h>

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = &arr[0];
    int sz = sizeof(arr)/sizeof(arr[0]);
    while(p<arr+sz) //指针的⼤⼩⽐较
    {
        printf("%d ", *p);
        p++;
    }

    return 0;
}

六、野指针

        概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

(一)野指针造成的原因

1、指针未初始化

        指针变量也是局部变量,不初始化就会给随机值;

        如果将未初始化的指针变量的值作为地址来进行解引用操作,就会形成非法访问

#include <stdio.h>

int main()
{ 
    int *p;//局部变量指针未初始化,默认为随机值
    *p = 20;

    return 0;
}

2、指针越界访问

#include <stdio.h>

int main()
{
    int arr[10] = {0};
    int *p = &arr[0];
    int i = 0;
    for(i=0; i<=11; i++)
    {
    //当指针指向的范围超出数组arr的范围时,p就是野指针
    *(p++) = i;
    }

    return 0;
}

3、指针指向的空间释放

#include <stdio.h>

int* test()
{
    int n = 100;
    return &n;
}

int main()
{
    int* p = test();
    printf("%d\n", *p);

    return 0;
}

(二)如何规避野指针

1、指针初始化

        如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL,NULL 是C语言中定义的⼀个标识符常量,值是0(这个0在C语言中会被强制转化为void*类型),0也是地址,这个地址是无法使用的,读写该地址会报错

        演示代码如下:

#include <stdio.h>
int main()
{
    int num = 10;
    int* p1 = &num;
    int* p2 = NULL;
 
    return 0;
}

2、小心指针越界

        ⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问

3、指针变量不再使用时,及时置NULL,指针使用之前检查有效性

        当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL;因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL

        演示代码如下:

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = &arr[0];
    for(int i = 0; i<10; i++)
    {
        *(p++) = i;
    }
    //此时p已经越界了,可以把p置为NULL
    p = NULL;

    //下次使⽤的时候,判断p不为NULL的时候再使⽤
    //...
    p = &arr[0];//重新让p获得地址
    if(p != NULL) //判断
    {
        //...
    }

    return 0;
}

4、避免返回局部变量的地址

        不要返回局部变量的地址

七、assert断言

        

        assert.h 头文件定义了宏 assert ( ) ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常常被称为“断言”

        使用:#include <assert.h>;assert(表达式)

        作用:判断是否符合指定条件,如果不符合就会终止运行;【通常用来判断指针变量的有效性】

        判断:判断为真则程序继续向下走,判断为假则报错

int* p = NULL;
...
assert(p != NULL); 
此处经过一些列的代码后,若 p 不等于NULL则正常运行下去,若还是等于NULL,则程序报错,终止运行

        若想取消assert断言,则在#include <assert.h>上面 #define NDEBUG;

        assert断言只在Debug版本中有效,在Release版本中会被优化掉

        缺点:引入了额外的检查,增加了程序的运行时间

八、指针的使用和传址调用

(一)指针的使用:strlen的模拟实现

        库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数

        函数原型如下:

size_t strlen ( const char * str );

        参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回长度;

        如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停止,代码如下:

size_t my_strlen(const char * str)
{
    int count = 0;
    assert(str);//为了保险,判断传来的是不是空地址
    while(*str)
    {
    count++;
    str++;
    }

    return count;
}

int main()
{
    size_t len = my_strlen("abcdef");
    printf("%zd\n", len);

    return 0;
}

        注:代码中的 const(不希望原值被修改)和 assert(保险判断)来加强代码使用时的健壮性(鲁棒性)
 

(二)传值调用和传址调用

        传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用,如果函数内部要修改主调函数中的变量的值,就需要传址调用


        以上内容仅供分析,若有错误,请多多指正


网站公告

今日签到

点亮在社区的每一天
去签到