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

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

所属专栏: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} 
arr12345678910
下标0123456789

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

代码参考如下:

#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) 不能返回长期变量的地址

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