【C语言】深入理解指针(1)

目录

1.内存和地址

2.指针变量和地址

2.1取地址操作符(&)

2.2指针变量和解引用操作符

2.2.1指针变量

2.2.2如何理解 int * p (如何拆解指针类型)

2.2.3解引用操作符(*)

2.2.4指针变量的大小

3.指针变量类型的意义

3.1指针+-整数

3.2void*

4.const修饰指针

4.1 const修饰变量

4.2 const修饰指针变量

5.指针运算

5.1指针+-指针

5.2指针-指针

5.3指针的关系运算

6.野指针

6.1野指针成因

1.指针未初始化

2.指针越界访问

3.指针指向的空间释放

6.2如何规避野指针

6.2.1指针初始化

6.2.2小心指针越界

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

7.assert断言

8.传值调用和传址调用


1.内存和地址

计算机中最小的单位是比特位bit,可以用来存储1个二进制位

1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB

内存单元编号 == 地址 == 指针

计算机中的编址,不是把每个字节的地址记录下来,而是通过硬件设计完成的。

地址总线:32位机器有32根地址总线,可以表示2^32种含义,每种含义表示一个地址。

2.指针变量和地址

2.1取地址操作符(&)

在C语言中创建变量的本质就是向内存申请空间。

%p可以用来打印地址

使用&取出地址(单目操作符)

&a取出的是a所占四个字节中较小的字节的地址

#include<stdio.h>
int main()
{
    int a = 0; // 为a申请4个字节的空间
    int *p = &a; 
    return 0;
}

2.2指针变量和解引用操作符

2.2.1指针变量

使用取地址操作符(&)取出的地址是一个数值,需要存储在指针变量中。

上述代码中,p是一个指针变量。指针变量也是变量,是用来存放地址的,存放在指针变量中的值会被理解为地址。

指针:地址

指针变量:存放地址的变量

口语中的指针一般指指针变量

2.2.2如何理解 int * p (如何拆解指针类型)

*说明p是一个指针变量

int说明p指向的对象是int类型

同样的,若p指向的类型是char类型,定义时要使用char*

2.2.3解引用操作符(*)

解作符(*)又叫间接访问操作符

*p表示通过p存放的地址找到p指向的对象

可以通过*p间接访问p指向的对象(例如可以通过*p=0将p指向的对象的值变为0)

*和&是可以相抵消的(*&a == a)

2.2.4指针变量的大小

32位机器假设有32根地址总线,每根地址总线的电信号转换成数字信号后都表示0或1,如果将32根地址总线产生的二进制序列当做一个地址,那么该地址就需要32个bit位进行存储,即32位机器的地址变量的大大小为4个字节。

同理,64位机器中的地址变量的大小为8个字节

指针变量的大小与其所指向的对象类型无关,只要是指针变量,在相同平台上的大小都是相同的。

如图,在x86(即32位)环境下,各种类型的指针变量大小均为4字节;在x64(即64位)环境下,均为8字节。

3.指针变量类型的意义

既然在同一个平台下各种类型的指针变量的大小是相同的,为什么还要有各种各样的指针类型呢?

因为指针类型决定了对指针进行解引用操作时有多大的权限。

比如:char*指针在解引用时只能访问一个字节,int*指针解引用时可以访问4个字节。

3.1指针+-整数

#include<stdio.h>
int main()
{
        int a = 10;
        int* pa = &a;
        char* pc = (char*) & a;
        printf("pa   = %p\n", pa);
        printf("pa+1 = %p\n", pa+1);
        printf("pc   = %p\n", pc);
        printf("pc+1 = %p\n", pc+1);
        return 0;
}

运行结果

可以看出,当int*类型的pa进行+1操作时,地址由006FFC08变为006FFC0C,向后移动了4个字节(sizeof(int));而char*类型的pc+1时,由006FFC08变为006FFC09,向后移动了1个字节(sizeof(char))。这就是不同指针变量类型的差别。

结论:指针类型决定了指针向前或向后一步有多大(字节)。

3.2void*

在指针类型中有一种特殊类型是void*类型的,可以理解为无具体类型的指针(或泛型指针)。

这种指针可以用来接受各种类型的地址,但是也有局限性:void*类型的指针不能直接进行指针+-整数和解引用的运算。

一般情况下,void*类型的指针使用在函数参数的部分,用来接收不同数据类型的地址,可以实现泛型编程的效果,使一个函数可以处理多种类型的数据。

4.const修饰指针

4.1 const修饰变量

const int a = 10; // a具有了常属性(不能被修改)

在c语言中,虽然a不能被修改了,但本质上还是变量(常变量);在c++中,const修饰的变量就是常量

被const修饰的变量可以通过指针修改(破坏规则)

4.2 const修饰指针变量

//代码1 
void test1()
{
    int n = 10;
    int m = 20;
    int* p = &n;
    *p = 20;//ok? 
    p = &m; //ok? 
}
void test2()
{
    //代码2 
    int n = 10;
    int m = 20;
    const int* p = &n;
    *p = 20;//报错
    p = &m; //ok? 
}
void test3()
{
    int n = 10;
    int m = 20;
    int* const p = &n;
    *p = 20; //ok? 
    p = &m; //报错 
}
void test4()
{
    int n = 10;
    int m = 20;
    int const* const p = &n;
    *p = 20; //报错 
    p = &m; //报错 
}
int main()
{
    //测试⽆const修饰的情况 
    test1();
    //测试const放在*的左边情况 
    test2();
    //测试const放在*的右边情况 
    test3();
    //测试*的左右两边都有const 
    test4();
    return 0;
}

一般来说,const可以放在*的左边,也可以放在*的右边

当const放在*左边,修饰的是指针指向的内容,确保指针指向的内容不能通过指针改变,但指针变量内容本身可以改变。

当const放在*右边,修饰的是指针变量本身,保证了指针变量本身的内容不能修改,但是指针指向的内容可以通过指针改变。

也可以同时放在*左边和右边,表示指针指向的内容和指针变量本身均不能修改。

5.指针运算

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

(1)指针+-指针

(2)指针-指针

(3)指针的关系运算

5.1指针+-指针

因为数组在内存中是连续存放的,所以只要知道第一个元素的地址,就可以找到后面所有元素。

Type * p;

p+1 --> 跳过 1*sizeof(type)

p+n --> 跳过 n*sizeof(type)

#include<stdio.h>
int main()
{
        int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
        int* p = &arr[0];
        int sz = sizeof(arr) / sizeof(arr[0]);
        for (int i = 0; i < sz; i++)
        {
                printf("%d\n", *(p + i));
        }
        return 0;
}

以上代码可用于输出数组元素

5.2指针-指针

|指针-指针| = 两指针间的元素个数

前提:两指针指向同一块空间(如指向同一个数组)

不存在指针+指针(无意义)

5.3指针的关系运算

指针的关系运算实际上就是指针和指针比较大小

可以用来判断指针是否指向数组内、

6.野指针

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

6.1野指针成因

1.指针未初始化

指针变量未初始化,默认为随机值

2.指针越界访问

eg.当指针指向的范围超出数组范围时就成为野指针

3.指针指向的空间释放

eg.函数结束后指针指向的变量被释放,p成为野指针

6.2如何规避野指针

6.2.1指针初始化

指针初始化有两种情况:

1.明确知道指针应该指向哪里,初始化一个明确的地址

2.暂时不知道指针应该指向哪里,初始化为NULL

NULL是C语言中定义的标识符常量,值为0,0也是地址,这个地址是无法使用的,读写该地址会报错

6.2.2小心指针越界

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

6.2.3指针变量不再使用时及时置为NULL,指针使用前检查有效性。

不再使用某个指针时要将指针置为NULL,使用指针前先判断指针是否为NULL

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

7.assert断言

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

assert(p) //判断p是否为空指针,等价于assert(p!=NULL)

assert()宏接受一个表达式作为参数(任意表达式)。如果该表达式为真(返回值非零),assert()不会有任何反应,程序继续运行;如果该表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示错误的表达式及其所在的文件名及行号。

使用assert()的好处:

1.可以自动标识文件和出问题的行号

2.如果已经确保程序没有问题,不再需要assert()断言,就在#include<assert.h>前定义一个宏NDEBUG。然后重新编译,编译器就会自动禁用文件中的所有assert()语句。

#define NDEBUG
#include<assert.h>

assert()的缺点:因为引入了外部的检查,增加了程序的运行时间。

在vs中,Release版本直接将assert()优化掉了,在linux版本中,Release版本仍会起作用。

8.传值调用和传址调用

传值调用:把变量本身传递给函数。实参传递给形参时,形参会单独创建一份临时空间来接收实参,对形参的修改不会影响实参。

传址调用:把变量的地址传递给函数。可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量。

如果函数中只是需要主调函数中的变量值进行运算,可以采用传值调用;如果函数内部要修改主调函数变量的值,就要采用传址调用。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值