理解C语言指针

1.指针和地址 

1.1概念 

在生活中,为了方便找到一个特定的房间,每一个房间设置了房间号,有了房间号,就能提⾼效率,能快速的找到房间

计算机的数据存储在内存当中,为了正确访问这些数据,每一个数据都标上号码,类似于房间号,每个号码是唯一的,根据这个号码,可以准确快速地找到数据,这个号码被我们叫做地址指针 

1.2指针变量 

 数据在内存中的地址,我们称为指针,存储了一份数据的指针(地址)的变量,叫做指针变量

写法: 

datatype* name

        //datatype表示 指针指向的数据类型,*表示这是一个指针变量

例子: 

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

1.3 *和& 

 此前在学习scanf函数的时候就见到过&,叫做取地址,,取变量的地址,*是解引用,与&的关系密切,意为获取地址关联的变量,&和*有一点点 相反功能 的意味

来看下面 的代码加深理解:

int a = 1;

int b = 3;

int c = 5;

int* pa = &a;   //将a的地址取出,存放在指针变量pa中,或者说:pa指向a

int* pb = &b;

int* pc = &c; 

printf("%d ", *pa);     

printf("%d ", *pb); 

printf("%d ", *pc); 

运行结果:1 3 5 

为什么呢?变量a,指针pa,最后*pa打印1 ;我们说,pa是一个指针变量,我们将pa指向了a,而后使用*pa,解引用pa进行输出;pa本是一个指针,解引用一次,*pa相当于就是a,而a = 1,最后打印的就是1,同理,变量b,指针pb;变量c,指针pc,也可做相同理解

1.4 NULL和viod* 

1.void*是一种特殊的指针类型,可以指向任意类型的数据,可以用任意类型的指针对 void 指针赋值 

int a = 0;

int* p1 = &a;

void* p2 = &a;

//这里的p1 与 p2 相等

void*的指针虽然可以指向任意类型的数据,但是不能把void*指针赋值给任意指针类型也不能对void类型的指针解引用,就是说,下面这种写法是错误的

void*p1;
int *p2;
p2=p1;     //不能这样赋值
*p1           //不能直接对void*解引用 

2.NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错,我们也将NULL称作空指针

int* p1 = NULL; 

 

1.5指针变量大小

整型、字符型、浮点型变量有大小, 指针变量同样有大小,但是与他们的大小的意义是截然不同的。这里我们要引入32位机器64位机器这个概念,这里32位,64位是机器的字长,那和指针大小有什么关系呢?

机器程序运行要有数据和地址,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。同理,64位机器需要8个字节才能存储

也就是说,32位环境底下,指针的大小是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 = 1;
    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;

32位底下,运行结果:

输出 :

    &n=005008D4
    p1=005008D4
p1+1=005008D5
    p2=005008D4
p2+1=005008D8

        //每次代码运行时,系统都会重新分配内存,所以输出结果每次都不会一样,但规律一样

先说结论:指针加减多少,就是指针向前后走多少步,而指针的类型决定了这“一步”有多长

char*类型的指针+1,跳过1个字节 

int*类型的指针+1,跳过 4个字节 

数组指针+1,跳过整个数组 

我们知道,数据在内存中是连续存储的,虽然数组指针+1是跳过整个数组,但是我们可以用数组元素类型的指针对数组的元素对数组的每一个元素进行访问

#include <stdio.h>
int main()
{
    int arr[] = {10,9,8,7,6,5,4,3,2,1 };
    int* p = &arr[0];       //使用int*类型的指针进行访问,恰好访问到每一个元素
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);   //计算数组元素个数
    for (i = 0; i < sz; i++)
    {
        printf("%d ", *(p + i));   
    }
    return 0;

运行结果:

10 9 8 7 6 5 4 3 2 1 

2.2指针-指针  

 先说结论:在同一空间内,指针减指针得到的是两个指针之间元素的个数

#include<stdio.h>

int main () 

{

        int arr[5] = {1,2,3,4,5};

        int* p1 = &arr[0];

        int* p2 = &arr[4];

        printf("%d", p2 - p1);

}

运行结果:4 

3.const修饰指针变量 

类比于字符串被const修饰,变成常量字符串,字符串不可被修改

const修饰指针变量也是如此,只是被修饰的可以以是指针变量本身,也可以是修饰指针指向的内容,所以有多种写法

3.1const在*左边

int a = 4;
const int * p1 = &a;
*p1 = 5;        //此行代码无效

因为const在 * 左边时,修饰指针指向的内容,保证指针指向的内容不能通过指针来改变

但是可以通过其他方式修改,如:a = 5

3.2const在*右边

int a = 4;
int b = 5;
int * const p1 = &a;
p1 = &b;        //此行代码无效 

const在 * 右边时,修饰的是指针变量本⾝,保证了指针变量的内容不能修改

3.3当 * 左右都有const修饰

 int a = 4;
int b = 5;
const int * const p1 = &a;
*p1 = 5;          //此行代码无效
p1 = &b;         //此行代码无效

这时,对指针变量p1的限制非常严格, 不仅指针指向的内容不能通过指针来改变,指针变量的内容也不能修改

4.野指针 

野指针就是:指针指向的位置是不可知道的(随机的,不正确的,无限制的)

4.1野指针形成原因 

1.指针没有初始化

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

2. 指针越界访问了

int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    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();   //n的地址只在test函数里起作用,此时已经释放
    printf("%d\n", *p);
    return 0;

 

4.2如何避免野指针 

1.指针初始化 ,或赋予NULL空指针(需要头文件stdio.h)

int  a = 4;

int* p = &a;

int* p1 = NULL 

2.小心指针越界

不超出范围进行访问,及时给指针赋予空指针(NULL),尽量不返回局部变量的地址

3.使用assert断言 

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏ 如下:

assert(p != NULL);

而且,在确定代码无问题的时候,无需找到曾经断言的地方,进行一一删除,只需要在头文件之前使用另一个宏NDEBUG 

#define NDEBUG

#include <assert.h>

 

5.传值调用和传址调用

顾名思义,传值调用,把值传过去使用;传址调用,把地址传过去使用。但是,在C语言中,这两者的意义完全不同

我们先看以下代码:

#include <stdio.h>
void Swap1(int x, int y)
{
    int tmp = x;
    x = y;
    y = tmp;
}
int main()
{
    int a = 2;
    int b = 5;
    printf("交换前:a=%d b=%d\n", a, b);
    Swap1(a, b);
    printf("交换后:a=%d b=%d\n", a, b);
    return 0;
}

这里代码本意是交换两个变量的值,但是 

 运行结果:

交换前:a=2 b=5
交换后:a=2 b=5

并没有交换,因为,这里采用的是传值调用,传的是形式参数,形式参数只是实际参数的一份零时拷贝文件,改变形式参数,并不影响实际参数的值,所以变量的值没有发生交换

若要交换,可以采用传址调用

#include<stdio.h>
void Swap2(int* px, int* py)
{
    int tmp = 0;
    tmp = *px;
    *px = *py;
    *py = tmp;
}
int main()
{
    int a = 3;
    int b = 4;
    printf("交换前:a=%d b=%d\n", a, b);
    Swap1(&a, &b);
    printf("交换后:a=%d b=%d\n", a, b);
    return 0;
}

运行结果:

交换前:a=3 b=4
交换后:a=4 b=3

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 46
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值