关于程序员:掌握C语言指针轻松解锁代码高效性与灵活性

88次阅读

共计 5326 个字符,预计需要花费 14 分钟才能阅读完成。

✨✨ 欢送大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C 语言学习
贝蒂的主页:Betty‘s blog

1. 指针与地址

1.1 概念

咱们都晓得计算机的数据必须存储在内存里,为了正确地拜访这些数据,必须为每个数据都编上号码,就像门牌号、身份证号一样,每个编号是惟一的,依据编号能够精确地找到某个数据。而这些编号咱们就将其称为 地址 或者 指针

1.2 指针变量

数据在内存中的地址称为指针,如果一个变量存储了一份数据的指针(地址),咱们就称它为 指针变量

那咱们如何应用指针变量呢?

  1. datatype *name;

*示意这是一个指针变量,datatype示意该指针变量所指向的数据的类型

例如:

int* p1;// 指向一个整型的指针
char* p2;// 指向一个字符的指针
float* p3;// 指向一个单精度浮点数的指针
double* p4;// 指向一个双精度浮点数的指针

1.3 & 和 *

咱们早在学习 scanf 时候就用过取地址符 &,它是将某个变量的地址取出来,而解援用 * 的意思就是 通过某个地址找到该地址存储的变量。可能解释起来比拟形象,咱们能够通过一个不失当的例子形象阐明一下。

首先咱们能够失去如下几个关系:

    int a = 1;// 第一个客户,&a 为 0x00000001
    int b = 2;// 第二个客户,&b 为 0x00000002
    int c = 3;// 第三个客户,&c 为 0x00000003

而后咱们能够通过指针变量把他们地址存储进去

    int* pa = &a;// 把 a 的地址存进去
    int* pb = &b;// 把 b 的地址存进去
    int* pc = &c;// 把 c 的地址存进去

在酒店中,咱们能够通过门牌号精确找到每个客户。同理,咱们也能够通过每个地址精确找到每个变量。

    printf("a=%db=%dc=%d", *pa, *pb, *pc);// 通过 * 解援用地址找到对应的值

输入后果 a=1 b=2 c=3

并且咱们能够通过指针变量进行赋值。

*pa = 4;
*pb = 5;
*pc = 6;
printf("a=%d b=%d c=%d\n", *pa, *pb, *pc);

输入后果:a=4 b=5 c=6

1.4 void* 指针和 NULL

(1)void* 是一种非凡的指针类型,它能够指向任意类型的数据,就是说能够用任意类型的指针对 void 指针赋值。

void*p1;
int*p2;
p1=p2;// 这是被容许的
  • 然而却 不能把 void* 指针赋值给任意指针类型,也不能间接对其解援用
void*p1;
int *p2;
p2=p1;// 不能这样赋值
*p1// 不能间接对 void* 解援用

(2)NULL 是 C 语⾔中定义的⼀个标识符常量,值是 0,0 也是地址,这个地址是⽆法使⽤的,读写该地址会报错

int*p=NULL;// 初始化指针

1.5 指针变量的大小

咱们晓得,当初常见的计算机分为 32 位机器64 位机器 。32 位机器假如有 32 根地址总线,每根地址线进去的电信号转换成数字信号后是 1 或者 0,那咱们把 32 根地址线产⽣的 2 进制序列当做⼀个地址,那么⼀个地址就是 32 个 bit 位,须要4 个字节能力存储。同理,64 位机器须要 8 个字节能力存储。

咱们能够通过以下代码来验证一下。

int main()
{printf("%zd", sizeof(char*));
    printf("%zd", sizeof(short*));
    printf("%zd", sizeof(int*));
    printf("%zd", sizeof(double*));
    return 0;
}

输入后果:

32 位机器:4 4 4 4

64 位机器:8 8 8 8

2. 指针的根本运算

2.1 指针 +- 整数

咱们先察看一下如下代码的地址变动

#include <stdio.h>
int main()
{
    int n = 10;
    char* p1 = (char*)&n;// 将 int* 强转为 char*
    int* p2 = &n;
    printf("%p\n", &n);
    printf("%p\n", p1);
    printf("%p\n", p1 + 1);//p1 向后挪动一位
    printf("%p\n", p2);
    printf("%p\n", p2 + 1);//p2 向后挪动一位
    return 0;
}

输入:

&n=005DF8D4
p1=005DF8D4
p1+1=005DF8D5
p2=005DF8D4
p2+1=005DF8D8

咱们能够看出,char 类型的指针变量 + 1 跳过 1 个字节,int 类型的指针变量 + 1 跳过了 4 个字节。由此咱们得出结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(间隔)。

  • 因为每次代码运行时,零碎都会从新分配内存,所以输入后果每次都不会一样,然而法则是一样的

咱们晓得数组在内存中是 间断存储 的(地址由低到高),所以咱们只须要只有首元素的地址就能找到数组所有元素的地址,而一维数组的数组名恰好就是咱们首元素的地址。

 假如有数组 int arr[10]={1,2,3,4,5,6,7,8,9,10} 
arr 1 2 3 4 5 6 7 8 9 10
下标 0 1 2 3 4 5 6 7 8 9

那咱们如何通过指针拜访每个元素呢?

代码参考如下:

#include <stdio.h>
int main()
{int arr[] = {1,2,3,4,5,6,7,8,9,10};
    int* p = &arr[0];//&arr[0]=arr
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);// 计算数组元素个数
    for (i = 0; i < sz; i++)
    {printf("%d", *(p + i));// 因为数组元素间断存储,所以能够通过 +- 整数找到之后元素
    }
    return 0;
}

输入后果:1 2 3 4 5 6 7 8 9 10

2.2 指针 - 指针

指针 - 指针其实是指在同一空间内,两个指针之间的元素个数

晓得这点之后,咱们可不可以本人实现一个字符串库函数 strlen()呢?

思路如下:

思路:首先定义两个指针 p1,p2,让两个指针指向首元素,而后让一个指针 p2 循环 ++,直到指向‘\0’就进行,最初返回 p2-p1,就能失去字符串的长度

代码如下:

int my_strlen(char* p1)
{
    char* p2 = p1;// 使两个指针都指向首元素
    while (*p2)
    {p2++;}
    return p2 - p1;// 返回两指针间接的元素的个数就是其长度
}
int main()
{char arr[] = "abcdef";
    int len = my_strlen(arr);// 计算 arr 字符串的长度
    printf("%d\n", len);
    return 0;
}
  • 参考贝蒂 string.h 大全

2.3 指针的关系运算

咱们晓得了指针变量实质是寄存的地址,而地址实质就是 十六进制 的整数,所以 指针变量也是能够比拟大小的

代码示例:

#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int* p = &arr[0];
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    while (p < arr + sz) // 指针的⼤⼩⽐较
    {printf("%d", *p);// 打印数组每个元素
        p++;
    }
    return 0;
}

3. const 润饰

咱们晓得变量是能够扭转的,然而在有些场景下,咱们不心愿变量扭转,那咱们该怎么办呢?这就是咱们接下来要讲的 const 的作用啦。

3.1 const 润饰变量

简略来说,通过 const 润饰的变量,能够当做一个常量,而常量是不能扭转的。

    int a = 1;// a 可批改的
    const int b = 2;
    b=3;// b 不可批改的

然而能够通过指针间接批改.

代码如下:

int main()
{
    const int b = 2;
    int* p = &b;
    *p = 3;// 通过指针间接批改
    return 0;
}

3.2 const 润饰指针

咱们晓得 const 的作用后,就能够看看上面几段代码。

    int a = 10;
    const int* p = &a;
    *p = 20;// 是否能够
    p = p + 1;// 是否能够

通过测试咱们发现,* p 无奈扭转成 20,然而 p 能够扭转成 p +1.

那如果把 const 调换一下地位,又会呈现什么状况呢~

    int a = 10;
    int* const p = &a;
    *p = 20;// 是否能够
    p = p + 1;// 是否能够

再次测试之后咱们发现,* p 能够被赋值为 20,然而 p 不能赋值为 p + 1 了

通过上述测试,咱们大抵能够总结出两个论断。

• const 如果放在的右边,润饰的是指针指向的内容,保障指针指向的内容不能通过指针来扭转。然而指针变量本⾝的内容可变。
• const 如果放在 * 的左边,润饰的是指针变量本⾝,保障了指针变量的内容不能批改,然而指针指向的内容,能够通过指针扭转。

4. assert 断言

assert 是一个宏,它的头文件为 <assert.h>, ⽤于在运⾏时确保程序合乎指定条件,如果不合乎,就报错终⽌运⾏。这个宏经常被称为“断⾔”。

举一个简略的例子:

assert(a>0);
  1. 如果 a 确实大于 0,assert 判断为真,就会通过。
  2. 如果 a 不大于 0,assert 判断为假,就会报错。

    所以 assert 经常用于查看空指针问题,以避免程序因为空指针的问题而出错。

int *p=NULL;
assert(p);// 空指针是 0,0 为假,就会报错

5. 传值调用与传址调用

5.1 传值调用

咱们后面学习函数时候,遇到过这样一段代码。

#include<stdio.h>
void swap(int x, int y)// 返回类型为 void 示意不返回值
{
    int temp = 0;// 定义一个长期变量
    temp = x;// 把 x 的值赋给 temp
    x = y;// 把 y 的值赋给 x
    y = temp;// 把 temp 的值赋给 y,实现替换操作
}
int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d", &a, &b);
    printf("替换前:a=%d,b=%d\n", a, b);
    swap(a, b);// 替换函数
    printf("替换后:a=%d,b=%d\n", a, b);
    return 0;
}

输出:3 5

输入:替换后 a =3,b=5

为什么两个值并没有替换呢,这是因为 形参只是实参的一份长期拷贝,对形参扭转,基本不会扭转实参。如果遗记的同学能够再去复习一下贝蒂的函数小课堂

5.2 传址调用

那咱们想在函数中扭转实参的值,那又该如何扭转呢?

其实很简略,咱们学了指针,晓得能够通过地址间接拜访该变量的值,所以咱们只须要把地址传给函数,在函数中通过地址拜访实参,并进行替换。

代码如下:

#include<stdio.h>
void swap(int*x, int*y)// 通过指针变量承受地址
{
    int temp = 0;// 定义一个长期变量
    temp = *x;// 把 * x 的值赋给 temp
    *x = *y;// 把 * y 的值赋给 *x
    *y = temp;// 把 temp 的值赋给 *y,实现替换操作
}
int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d", &a, &b);
    printf("替换前:a=%d,b=%d\n", a, b);
    swap(&a, &b);// 将地址传给函数
    printf("替换后:a=%d,b=%d\n", a, b);
    return 0;
}

6. 野指针

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

6.1 野指针成因

(1)指针未初始化
#include <stdio.h>
int main()
{
    int* p; // 局部变量指针未初始化,默认为随机值
    *p = 20;
    return 0;
}

因为 p 是随机值,所以对 p 解援用,零碎无奈通过 p 的地址找到对应的空间,所以出错造成野指针

(2)数组越界拜访
#include <stdio.h>
int main()
{int arr[10] = {0};
    int i = 0;
    for (i = 0; i < 11; i++)
    {
        // 数组下标是 0 到 9
        printf("%d", *(arr + i));
    }
    return 0;
}

  • 个别呈现这种较大的随机值,个别都是数组越界拜访
(3)指针指向空间开释
#include <stdio.h>
int* test()
{
    int n = 10;
    return &n;// 返回 n 的地址
}
int main()
{int* p = test();// 用 p 承受 n 的地址
    printf("%d\n", *p);// 打印出 n 的值
    return 0;
}

这段代码乍一看,如同并没有什么问题,然而大家在学习函数的时候晓得,在函数中定义的变量是长期变量,一旦 出了作用域就会销毁。

一旦销毁,零碎就无法访问该空间,而通过指针咱们还能够拜访该空间,这就造成了抵触,所以出错,造成野指针。

6.2 解决办法

(1) 初始化

NULL 是 C 语⾔中定义的⼀个标识符常量,值是 0,0 也是地址,这个地址是⽆法使⽤的,读写该地址会报错。如下是 NULL 在编译器中的定义:

ifdef __cplusplus

define NULL 0

else

define NULL ((void *)0)

endif

#include <stdio.h>
int main()
{
    int* p=NULL;// 用空指针初始化,让其有指向地位
    //*p = 20;NULL 地址不能读写
    return 0;
}
(2) 小心越界拜访

咱们在应用数组时候,肯定要对数组的元素个数有一个清晰的把控,不然就很容易呈现越界拜访的状况。

(3) 不能返回长期变量的地址

长期变量出了作用域就会销毁,零碎会回收该空间,所以咱们要尽量避免指针指向曾经销毁的空间,尤其在函数中,不能返回长期变量的地址。

正文完
 0