深入理解指针(2)

发布于:2025-05-09 ⋅ 阅读:(8) ⋅ 点赞:(0)

目录

1.const 修饰指针

1.1 const修饰变量

​编辑 1.2 const 修饰指针变量

2. 野指针

2.1  野指针成因

1.指针未初始化

2 指针越界访问

3 指针指向的空间释放

2.2 如何规避野指针

2.2.1 指针初始化

2.2.2 ⼩⼼指针越界

2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

2.2.4 避免返回局部变量的地址

3 assert 断⾔

4 指针的使⽤和传址调⽤

4.1 strlen的模拟实现

4.2 传值调⽤和传址调⽤


1.const 修饰指针

1.1 const修饰变量

const常属性的意思就是不能改变的意思

加了const就不可以修改a

c语言中

1.这里的a本质上还是变量

2.只是在语法层面上限制了a的修饰

a是常变量

 1.2 const 修饰指针变量

int main()
{
    const int a = 10;
    int* pa = &a;
    *pa = 20;
    printf("%d\n", a);
    return 0;
}

限制的是*pa

不限制pa

限制的是pa

不限制*pa

总结:

const在修饰指针变量的时候:
const 可以放在*的左边,const限制的是pa指向的对象,也就是*pa不能给修改
但是pa不受限制,也就是指针变量可以改变指向
const 可以放在*的右边,const限制的是pa,也就是pa的指向不能改变了
但是*pa不受限制,也就是说pa指向的内容,是可以通过pa来改变的

 

2. 野指针

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

指针指向的空间,是不属于当前程序的

2.1  野指针成因

1.指针未初始化

之前的

pa是变量,局部变量

局部变量不初始化,它里边放的是一个随机值

指针越界访问

循环到第11次的时候,p就是野指针了

3 指针指向的空间释放

int* test()
{
    int n = 100;
    //...
    return &n;//int*
}
int main()
{
    int*p = test();
    printf("hehe\n");//加了这行代码打印出来*p的值不是100,不加打印出来是100
    printf("%d\n", *p);
    return 0;
}

n出函数已经销毁

2.2 如何规避野指针

2.2.1 指针初始化

明确知道,指针变量要初始化为 什么值

int a = 10;

int *pa = &a;

不知道指针变量应该给什么值

int *p =NULL;//空指针

意思是p没有指向有效的空间

暂时不要使用它

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。

2.2.2 ⼩⼼指针越界

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

2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

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

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr;
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        *p = 5;
        p++;
    }
    p = NULL;
    //现在又想使用p
    p = arr;
    if (p == NULL)
    {
        printf("p 是空指针\n");
    }
    else
    {
        //不为空指针
    }
    return 0;
}

返回局部变量 

利用寄存器

返回局部变量的地址--err错

等于返回栈空间地址

2.2.4 避免返回局部变量的地址

如造成野指针的第3个例⼦,不要返回局部变量的地址。

assert 断⾔

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“ 断⾔ ”。
assert(p != NULL);
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣
任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

assert的头文件#include <assert.h>
#include <assert.h> 

为真不报错

为假会报错

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和
出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG
#define NDEBUG
#include <assert.h>
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响⽤⼾使⽤时程序的效率。

4 指针的使⽤和传址调⽤

4.1 strlen的模拟实现

之前的

strlen是求字符串的长度的

只需要将字符串的起始地址传递给strlen就行

统计的是字符串中\0之前的字符的个数

 size_t无符号的整型

size_t my_strlen(const char* str)
{
    size_t count = 0;
    assert(str != NULL);
    while (*str != '\0')
    {
        count++;
        str++;
    }
    return count;
}
 
int main()
{
    char arr[] = "abcdef";
    size_t len = my_strlen(arr);
    printf("%zu\n", len);

    return 0;
}

4.2 传值调⽤和传址调⽤

写⼀个函数,交换两个整型变量的值

代码1: 

void Swap1(int x, int y)
{
    int z = 0;
    z = x;
    x = y;
    y = z;
}

int main()
{
    int a = 10;
    int b = 20;
    printf("交换前: a = %d b = %d\n", a, b);
    //
    Swap1(a, b); //传值调用
    printf("交换后: a = %d b = %d\n", a, b);

    return 0;
}

可以看出没有交换

Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这种叫 传值调⽤

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。

所以Swap1是失败的了。

代码2:

void Swap2(int* pa, int * pb)
{
    int z = 0;
    z = *pa;//z = a
    *pa = *pb;//a = b
    *pb = z;
}

int main()
{
    int a = 10;
    int b = 20;
    printf("交换前: a = %d b = %d\n", a, b);
    //
    Swap2(&a, &b); //传址调用
    printf("交换后: a = %d b = %d\n", a, b);

    return 0;
}

成功交换了 

这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫: 传址调⽤。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。

网站公告

今日签到

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