C语言指针详解

目录

1.指针变量

I.内存和地址(补充):

II.编址(补充):

III.二级指针 

2.指针操作符

I.取地址符(&)

II.解引用操作符(*)

III.成员访问操作符(->)

IV.下标访问操作符([])

3.指针变量类型

I.字符指针变量

II.数组指针变量

III.函数指针变量

IV.void型指针

4.野指针(悬空指针)

I.野指针成因:

(1).指针未初始化:

(2).指针越界访问:

(3).指针指向的空间释放:

II.规避野指针

(1).初始化:

(2).注意指针越界:                                                                                                                             

(3).指针变量不再使用时候,及时置为NULL;使用之前检查指针有效性:                                                                                                                                

(4).避免返回局部变量地址:

5.Assert断言

6.Const修饰指针

7.指针大小 

 8.指针运算

I.指针+-整数

II.指针的关系运算


1.指针变量

在C语言中,创建变量要向内存申请空间.比如在学习scanf输入函数的时候其实就已经接触到指针了

下面两组代码是一样的效果

	int a = 0;
	scanf("%d", &a);
	int a = 0;
	int* p = &a;
	scanf("%d", p);

&操作符需要取出变量的地址,但是在语句中,我们只是将其作为一个占位符,没有将它放到具体的变量中(没有将地址存放到变量中),如果后续我们要用到取出的地址,就要将地址存放一定起来,如何存放呢?当然就是存放在指针中,指针也是一种变量,用来专门存放地址的一种变量,存放在指针中的值都会理解为地址

I.内存和地址(补充):

  • 计算机CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据,处理后的数据也会放回内存中.那这些空间是如何高效的管理呢?
  • 其实就是把内存划分为一个个的内存单元,每个单元的大小取1个字节(补充计算机常见单位),一个字节可以放8个比特位,一个比特位可以存储一个二进制的1或者0. 为了方便查找数据,每个内存单元都有一个编号(就像门牌号),由这些0或1组成内存单元的编号,但是一般不以二进制(0/1)显示,以十六进制显示.有了这些内存单元的编号,CPU就可以快速的找到一个内存空间.在计算机中把这些内存的编号成为地址(就像生活中把门牌号叫做地址).C语言给地址起了新的名字:指针.利用指针,程序可以轻松地处理复杂的数据结构和动态分配的内存空间,提高了程序的灵活性和效率.

所以可以这样理解:内存单元==地址==指针

II.编址(补充):

  • 那计算机是如何给内存中的每个内存单元分配唯一的地址的呢?这就不得不提到编址,这里做一个简单的介绍,当我们了解编址是给每个内存单元分配唯一地址的过程后,就可以更好的理解指针是如何工作的.
  • CPU访问内存的某个字节空间,必须知道这个字节空间在内存的什么位置,因而内存中字节很多,所以要给内存编址,但是要注意的是,计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的.
  •  硬件与硬件之间是靠线连接起来的,在CPU和内存之间也一样,两者要用线连起来.简单的理解编址,知道地址总线即可,对于不同位数的机器,可以这样理解,32位机器有32根地址总线,每根线只有两种态,表示0,1(电脉冲有无),那么一根线,就能表示2中含义,2根线4中含义.......32根地址线,就可以表示2^32种含义,每种含义代表一个地址.地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据通过数据总线传入CPU寄存器

III.二级指针 

既然指针是变量,那指针也有地址,那指针的地址存放在哪呢?没错,就是二级指针

#include <stdio.h>

int main() 
{
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;

    return 0;
}
  •  *ppa通过对ppa中的地址解引用,这样找到的是pa,*ppa其实访问的就是pa
  • **ppa先通过*ppa找到pa,然后对pa进行解引用操作:*pa,找到的就是a
  • *ppa==pa        **ppa==*(*ppa)==*pa==a

2.指针操作符

I.取地址符(&)

取地址符用于取出变量的地址,通过取地址符,可以将变量的地址赋值给指针变量.

int x=10;
int *ptr=&x;//ptr指向x的地址

II.解引用操作符(*)

解引用操作符用于访问指针所指向地址的值.通过解引用操作符,可以获取指针所指向的地址上储存的值.

int y=*ptr;//y的值为10,因为ptr指向变量x,*ptr获取x的值

III.成员访问操作符(->)

成员访问操作符用于通过指针访问结构体或类的成员.通过这个操作符,可以访问到其中的成员变量.

structure Person{
    char name[20];
    int age;
};
struct Person person;
struct Person *ptr = &person;
ptr->age=25//通过指针ptr访问结构体成员age

IV.下标访问操作符([])

下标操作符主要是用于通过指针访问数组元素.通过指针和下标操作符,可以访问数组中特定的元素

3.指针变量类型

I.字符指针变量

int main()
{
 char ch = 'w';
 char *pc = &ch;//取出ch的地址,指向ch的地址
 *pc = 'w';//解引用获取w
 return 0;
}

和字符相关的指针,这是个容易让人误解的代码: 

int main()
{
 const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
 printf("%s\n", pstr);
 return 0;
}

pstr事实上只是储存了"hello bit"中的首元素的地址,使用%s打印的时候,会往后一直打印知道遇到字符串结束符为止. 

II.数组指针变量

类比的思想:

整型指针变量: int * pint; 存放的是整型变量的地址,能够指向整型数据的指针
浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量
int *p[10];
p先和*结合,说明p是⼀个指针变量,然后指向的是⼀个大小为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针.换个方式理解:去掉名字就是变量类型:int *[10],所以p的类型是一个指向大小为10个整型数组的整型数组指针.
数组指针变量的初始化:
int arr[10] = {0};
&arr;//得到的就是数组的地址
int(*p)[10] = &arr;//只初始化了一个

注意:

  • 数组名就是首元素的地址,所以在代码中,p[0]就等于arr,但是需要注意两点数组名不是首元素的地址

  • sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
  • &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的,后面讲指针运算会说到)

III.函数指针变量

当创建一个函数的时候,函数有没有地址呢?写一个简单的代码测试:

int Add(int x, int y)
{
	return (x + y);
}
int main()
{
	Add(3, 4);
	printf("&Add==%p\n", &Add);
	printf(" Add==%p\n", Add);
	return 0;
}

通过调试代码可以看出:

函数也有地址,函数名就是函数的地址,当然也可以通过&函数名的方式获得函数的地址,同理函数的地址如果需要存放起来的话,就要用到函数指针变量.在调试窗口中,可以看到&Add的类型是int(*)(int,int),上面说到了去掉名字就是类型名,这里也是如此,函数指针的写法和数字指针非常相似,使用如下:

void test()
{
 printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
 return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
int(*pf3)(int x,int y);

 

可以通过函数指针调用所指向的函数 :

#include <stdio.h>
int Add(int x, int y)
{
 return x+y;
}
int main()
{
 int(*pf3)(int, int) = Add;
 
 printf("%d\n", (*pf3)(2, 3));
 printf("%d\n", pf3(3, 5));
 return 0;
}

 结果:

IV.void型指针

在指针类型中有一种特殊的类型是void*类型的,可以理解为无具体类型的 指针(或者叫泛型指针),这种类型的解引用可以用来接收任意类型的地址.但也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算(要使用需进行类型转换).

#include <stdio.h>
int main()
{
 int a = 10;
 void* pa = &a;
 *pa = 10;//这样的写法是错误的,如要使用pa,需要将其强制转换-->*(int*)pa
 return 0;
}
  • void*类型的指针,可以接受不同类型的地址,但是无法进行指针运算.
  • 一般void*类型的指针是使用在函数参数部分,用来接收不同数据类型的地址.

4.野指针(悬空指针)

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

I.野指针成因:

  1. (1).指针未初始化:
    #include<stdio.h>
    int main()
    {
        int *p;//局部变量指针未初始化,默认为随机值
        *p=20;
        return 0;
    }
  2. (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;
    	}
    	return 0;
    }
  3. (3).指针指向的空间释放:
    #include<stdio.h>
    int* test()
    {
    	int n = 100;
    	return &n;
    }
    int main()
    {
    	int* p = test();
    //虽然能正常打印100,是因为在程序运行过程中,释放的内存还没有被覆盖,所以数据还保留在那个位置
    	printf("%d\n", *p);
    	return 0;
    }

II.规避野指针

  • (1).初始化:
    #include<stdio.h>
    int main()
    {
        int num=10;
        int *p1=&num;
        int *p2=NULL;
        return 0;
    }
  • (2).注意指针越界:                                                                                                                       

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

  • (3).指针变量不再使用时候,及时置为NULL;使用之前检查指针有效性:         

 当指针变量指向一块区域的时候,我们通过指针访问该区域,后期不再使用这个指针访问空间的时候,我么可以把该指针置为NULL.

  • (4).避免返回局部变量地址:

如造成野指针的第三个例子,不要返回局部变量的地址.

5.Assert断言

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

assert(p!=NULL);

上面代码在程序运行到这行语句时,验证变量p是否等于NULL.如果不等于NULL;程序继续运行,否则就终止运行,并给出报错信息提示.

assert宏接受一个表达式作为参数.如果该表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行.如果表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号.

assert的好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭的assert()机制.如果已经确认程序没有任何问题,不需要做断言,就在#include<assert.h>的前面,定义一个宏DEBUG.

#define DEBEG
#include<assert.h>

6.Const修饰指针

变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量.但是如果希望一个变量加上一些限制,不能被修改,怎么做呢?这就是const的作用.

#include<stdio.h>
int main()
{
    int m=0;
    m=20;//m是可以修改的
    const int n=0;
    n=20;//n是不能被修改的
    return 0;
}

上述代码中的n是不可以被修改的,其中n本质上是变量,只不过被const修饰后,在语法上加上了限制,只要对n进行修改,就不符合语法规则,就报错,致使无法直接修改n.

但是如果绕过n,使用n的地址去修改n就能做到,虽然这样是在打破语法规则

#include<stdio.h>
int main()
{
    const int n=0;
    printf("n=%d\n",n);
    int *p=&n;
    *p=20;
    printf("n=%d",n);
    return 0;
}

这样绕过n,通过利用地址的方式可以修改n的值,那有没有一种办法,能够让拿到地址也不能修改(就像上面的给变量加上const一样)呢? 

void test1()
{
	int n = 10;
	int m = 20;
	int *p = &n;//ok?
	p = &m;//ok?
}
void test2()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;//ok?
	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;//ok?
	p = &n;//ok?
}
int main()
{
	//测试无const修饰的情况
	test1();
	//测试const放在*左边的情况
	test2();
	//测试const放在*右边的情况
	test3();
	//测试*的左右两边都有const
	test4();
}

在test1函数中,变量m,n和指针p都没有被const修饰,可以对它们的值进行修改,指针p可以指向n,也可以指向m.

test2函数将const放在*的左边,此时如果对指针p指向的内容进行修改,就会报错.

test3函数将const放在*的右边,此时如果改变指针变量p的内容,就会报错.

test4函数中,*的左边右边都有const修饰.,既不可以对p指向的内容进行修改,也不可以修改指针变量p的内容.

  • 总结:
    const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变.但是指针变量本身的内容可变.
  • const如果放在*的右边,修饰的是指针变量本身,保证指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变.

如果将代码放到VSstudio2022中,可以很好地看出问题:

7.指针大小 

在C语言中,指针的大小取决于计算机架构和操作系统.通常情况下,指针的大小和计算机的处理器位数相关.

32位平台下地址是32个bit位,指针变量大小是4个字节

64位平台下地址是64个bit位,指针变量大小是8个字节

C语言中,指针的大小主要受到底层计算机硬件和操作系统的限制,不受指向的变量类型的影响.(一个整型变量的指针和一个字符型变量的指针,它们在内存中占用的空间大小是一样的(同一环境下)

 8.指针运算

I.指针+-整数

这里的内容借助数组来说明,因为数组在内存中是连续存放的,只要知道第一个元素的地址,顺着就可以找到后面的所有元素.

 先来看这样的一段代码:

#include<stdio.h>
int main()
{
	int n = 10;
	char* pc = (char*)&n;//pc的类型是字符类型
	int* pi = &n;//pi的类型是整型的
	printf("%p\n", &n);
	printf("%p\n", &pc);
	printf("%p\n", &pc+1);
	printf("%p\n", &pi);
	printf("%p\n", &pi+1);
}

 运行结果:

 从结果可看出,指针变量类型的差异带来的变化是:

  • 指针的类型决定了指针向前或者向后走一步有多大(距离)
  • 指针类型决定了对指针的解引用的时候有多大的权限(一次能操作几个字节)
    比如char*的指针解引用就只能访问一个字节,int*的指针解引用就能访问四个字节.

开始提到的数组就可以利用指针的运算进行访问:

#include <stdio.h>
int main() {
    int array[] = {10, 20, 30, 40, 50};
    int *ptr = array; // 指向数组第一个元素的指针

    // 使用指针进行数组元素的遍历和访问
    for (int i = 0; i < 5; i++) {
        printf("Element %d: %d\n", i, *ptr);
        ptr++; // 指针后移,访问下一个元素
    }

    // 使用指针进行倒序遍历和访问数组元素
    ptr = &array[4]; // 指向数组最后一个元素的指针
    for (int i = 4; i >= 0; i--) {
        printf("Element %d: %d\n", i, *ptr);
        ptr--; // 指针前移,访问前一个元素
    }

    return 0;
}

在这里解释一下前面说到的 整个数组的地址和数组首元素的地址的区别:

#include <stdio.h>

int main() {
    int arr[5] = { 1, 2, 3, 4, 5 };
    int(* ptr_arr)[5] = &arr; // 指向整个数组的指针
    int(*ptr_first)[1] = &arr[0]; // 指向数组首元素的指针

    // 输出整个数组的地址和数组首元素的地址
    printf("整个数组的地址:%p\n", (void*)ptr_arr);
    printf("数组首元素的地址:%p\n", (void*)ptr_first);

    // 对指向整个数组的指针执行+1操作
    ptr_arr = ptr_arr + 1;

    // 对指向数组首元素的指针执行+1操作
    ptr_first = ptr_first + 1;

    // 输出移动后的地址
    printf("数组首元素的地址+1:%p\n", (void*)ptr_first);
    printf("整个数组的地址+1:%p\n", (void*)ptr_arr);

    return 0;
}

 

II.指针的关系运算

#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;
}

arr+sz表示的是arr中第sz个元素的下一个的地址,通过比较指针的方式遍历数组将其打印

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值