深入理解指针(1)---学习笔记分享

深入理解指针(1) 学习笔记

1.1内存与指针的浅显理解

1.内存划分为一个个的内存单位(1个字节),一个字节可以放八个比特位

2.内存单元的编号==地址==指针

1.2如何理解编址

1.地址总线

2.数据总线

3.控制总线

每根线只有两态,地址信息被下达给内存,就可以找到该地址对应的数据,将数据再通过数据总线传入cpu内寄存器;

2.1和2.2

变量创建的本质就是在内存里面申请空间;

eg1:int a = 20;//向内存申请四个字节的空间,用来存放int类型的数值;这四个字节,每个字节都有地址;变量的名字仅仅是给程序员看的,编译器不看名字,编译器是通过地址找内存单元的;&a就能拿到变量a的地址;

int * pa = &a;pa是一个变量,这个变量是用来存放地址(指针)的,所以pa叫指针变量(名字),int *是pa的类型;

//*表示pa是指针变量

//int 表示pa指向的变量a的类型是int

*pa// *--解引用操作符(间接访问操作符)

2.3指针变量的大小

前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或0,那我们把32根地址线产生的2进制序列当作一个地址,那么一个地址就是32个bit位,需要4个字节才能储存。

如果是64位机器,一个地址就是64个二进制位组成的二进制序列,储存起来就需要8个字节的空间,指针变量的大小就是8个字节。

int a = 10;
int* p = &a;
//1.指针变量是用来存放地址的,地址的存放需要多大空间,那么指针变量的大小就是多大
printf("%zd\n",sizeof(p));
char ch = 'w';
char * pc = &ch;
printf("%zd\n",sizeof(pc));
//指针变量大小跟类型无关,x86就是4,x64就是8,与类型无关
3.指针类型的意义
3.1指针的解引用
//代码1
#include<stdio.h>
int main()
{
    int n = 0x11223344;
    int *pi = &n;
    *pi = 0;
    return 0;
}
//代码2
#include<stdio.h>
int main()
{
    int n = 0x11223344;
    char *pc = (char *)&n;
    *pc = 0;
    return 0;
}

上述代码我们调试之后我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第一个字节改为0。

结论:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字符)。比如:char的指针解引用就只能访问一个字节,而int的指针的解引用就能访问四个字节。
3.2指针+/-整数

int * pa ; pa + 1 -----> +1*sizeof(int)

pa + n------> +n*sizeof(int)

char * pc; pc + 1 -------> 1*sizeof(char)

pc + n -------> n*sizeof(char)

指针的类型决定了指针向前或者向后走一步大概有多大(距离)
3.3 void*指针

无具体类型的指针--void* (泛型指针)

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

int a = 10;
char ch = 'w';
void* pv1 = &a;//int*
void* pv2 = &ch;//char*
4.const修饰指针
4.1const修饰普通变量

变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以改变这个变量。但是如果我们希望一个变量加上一些限制,让它不能被修改--------const的作用

//代码1
int num = 100;
num = 200;
const int n = 10;//不可以被修改,具有常属性;const修饰变量的时候叫:常变量(本质上还是变量)
n = 20;//编译的时候会在这里报错
//代码2(const锁了门,但还可以翻窗户间接改)
const int n = 10;
int* p = &n;
*p = 200;
printf("%d\n",n);//打印200
4.2const修饰指针变量

一般来讲const修饰指针变量,可以放在左边,也可以放在右边,意义是不一样的

int main()
{
    int n = 10;
    int m = 100;
    //const放在*左边限制的是*p,但没有限制p;限制的是指针指向的内容,不能通过指针变量来修改它指向的内容
    //但是指针变量本身是可以改变的
    int const*p = &n;//const放在*左边的第一种情况
    const int* p = &n;//const放在*左边的第二种情况
    *p = 20;//err,const限制了*p
    p = &m;//ok,不会报错
    return 0;
}
int main()
{
    int n = 10;
    int m = 100;
    //const放在*右边限制的是p,但没有限制*p
    //限制的是指针变量本身,指针不能改变它的指向,但是可以通过指针变量修改它所指向的内容
    int * const p = &n;//const放在*右边
    *p = 20;//ok,不会报错err                
    p = &m;//err,const限制了p
    
    return 0;
    
}

关于指针p有3个相关的值

1.p,p里面放着一个地址

2.*p,p指向的那个对象

3.&p,表示的是p变量的地址

结论:
1.const放在*左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。这种声明中,const修饰的是指针指向的数据。这意味着指针p指向的数据是常量,不能通过p来修改。但是,指针p本身可以被修改,即你可以让p指向另一个地址。(房间里的人不能变,但门牌号可以变)。
2.const放在*的右边,修饰的是指针变量本身,保证了指针比那辆的内容不能修改,但是指针指向的内容,可以通过指针改变;这种声明中,const修饰的是指针变量本身。这意味着指针p的值(即它所指向的地址)是常量,不能被修改。但是,指针指向的数据可以被修改。(房间的门牌号不能变,但房间里面的人可以变)。
5.指针运算

1.指针 +- 整数;

2.指针 - 指针;

3.指针的关系运算;

5.1指针 +- 整数

1.指针类型决定了指针+1,的

步长;还决定了指针解引用的权限

2.数组在内存中连续存放的

5.2指针 - 指针

指针-指针的绝对值是指针和指针之间元素的个数(计算的前提条件是两个指针指向的是用一个空间)

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    printf("%d\n",&arr[9] - &arr[0]);//打印9
    return 0;
}

//写一个函数求字符串的长度

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

//写一个my_strlen函数
//第一种写法
size_t my_strlen(char* p)//size_t是一个无符号整型
{
    int count = 0;
    while(*p != '\0')
    {
        count++;//计数器
        p++
    }
    return count;
}
​
//第二种写法
size_t my_strlen(char* p)//size_t是一个无符号整型
{
    char* start = p;
    char* end = p;
    while(*end != '\0')
    {
        end++;
    }
    return end - start;
}
​
int main()
{
    char arr[] = "abcdef";
    size_t len = my_strlen(arr);//数组名其实是数组首元素的地址 arr == &arr[0]
    printf("%zd\n",len);
    return 0;
}
5.3指针的关系运算
#include<stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int sz = sizeof(arr) / sizeof(arr[0]);
    int* p = &arr[0];
    while(p < &arr[sz])//指针的关系运算
    {
        printf("%d ",*p);
        p++;
    }
    return 0;
}
6.野指针

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

6.1野指针成因

1.指针未初始化

#include<stdio.h>
int main()
{
    int* p;//p是局部变量,但是没有初始化,其值是随机的;
    //如果将p中存放的值当作地址,解引用操作符就会形成非法访问
    *p = 10;//p就是野指针
    return 0;
}

2.指针越界访问

#include<stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = &arr[0];
    int i = 0;
    for(i = 0;i <= 11;i++)
    {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
    }
    int* p;
    *p = 10;
    return 0;
}
6.2如何避免野指针
6.2.1指针初始化

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针辅助NULL

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

#includem<stdio.h>
{
    int a = 10;
    int *p = &a;
    int *p2 = NULL;
    *p2 = 200;//err
}
6.2.2小心指针越界

不能超出范围访问

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

约定俗成的一个规则:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL

if(*p != NULL)
{
    *p = 200;
}
6.2.4避免返回局部变量的地址
7.assert断言

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

assert(p != NULL);

上面代码在程序运行到这一行语句时,验证变量p是否等于 NULL、如果确实不等于 NULL,程序继续运行,否则就会终止运行,并且给出报错信息提示, assert()宏接受一个表达式作为参数。如果该表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零),assent()就会报错,在标准错误流 stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。 assert()的使用对程序员是非常友好的,使用 assert()有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assent()的机制。如果已经确认程序没有问题,不需要再做断言,就在#include <assert.h>语句的前面,定义一个宏 NDEBUG。

#define NDEBUG
#include<assert.h>

assert的缺点是因为引入了额外的检查,增加了程序的运行时间

一般可以在Debug中使用,再Release中选择禁用assert就行

8.指针的使用和传址调用
8.1 strlen的模拟实现
//求字符串长度
//参数p指向的字符串不期望被修改,在前面加const
size_t my_strlen(const char* p)//size_t是一个无符号整型
{
    size_t count = 0;
    assert(p != NULL);//检测指针p是否有效//使代码健壮性和鲁棒性(更稳定)
    while(*p)
    {
        count++;//计数器
        p++
    }
    return count;
}
​
int main()
{
    char arr[] = "abcdef";
    size_t len = my_strlen(arr);//数组名其实是数组首元素的地址 arr == &arr[0]
    printf("%zd\n",len);
    return 0;
}
8.2 传值调用和传址调用

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

#include<stdio.h>
void Swap1(int x,int y)
{
    int z = 0;
    z = x;
    x = y;
    y = z;
}
​
int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d",&a,&b);
    printf("交换前:a=%d b=%d\n",a,b);
    Swap1(a,b);
    printf("交换后:a=%d b=%d\n",a,b);
    return 0;
}
//发现交换失败
//当实参传递给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参
#include<stdio.h>
void Swap2(int *pa,int *pb)
{
    int z = 0;
    z = *pa;
    *pa = *pb;
    *pb = z;
}
​
int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d",&a,&b);
    printf("交换前:a=%d b=%d\n",a,b);
    Swap2(&a,&b);
    printf("交换后:a=%d b=%d\n",a,b);
    return 0;
}

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

结论:实参传递给形参的时候,形参会单独创建一份临时空间来接受实参,对形参的修改不影响实参。
所以Swap1是失败的。

Swap2函数将变量的地址传递给了函数:这种函数调用方式叫传址调用

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值