C++指针详解

2023-05-16

概述

C/C++语言之所以强大,以及其自由性,很大部分体现在其灵活的指针运用上。因此,说指针是C/C++语言的灵魂一点都不为过。 有好的一面,必然会有坏的一面,指针的灵活导致了它的难以控制,所以C/C++程序员的很多bug是基于指针问题上的。今天就对指针进行详细的整理。

1、指针是什么?

指针是“指向(point to)”另外一种类型的复合类型。复合类型是指基于其它类型定义的类型。

理解指针,先从内存说起:内存是一个很大的,线性的字节数组。每一个字节都是固定的大小,由8个二进制位组成。最关键的是,每一个字节都有一个唯一的编号,编号从0开始,一直到最后一个字节。

程序加载到内存中后,在程序中使用的变量、常量、函数等数据,都有自己唯一的一个编号,这个编号就是这个数据的地址

指针的值实质是内存单元(即字节)的编号,所以指针单独从数值上看,也是整数,他们一般用16进制表示。指针的值(虚拟地址值)使用一个机器字的大小来存储,也就是说,对于一个机器字为w位的电脑而言,它的虚拟地址空间是0~[2的w次幂] - 1,程序最多能访问2的w次幂个字节。这就是为什么xp这种32位系统最大支持4GB内存的原因了。
在这里插入图片描述

因此可以理解为:指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。

2、变量在内存中的存储

举一个最简单的例子 int a = 1,假设计算机使用大端方式存储:
在这里插入图片描述
内存数据有几点规律:

  1. 计算机中的所有数据都是以二进制存储的
  2. 数据类型决定了占用内存的大小
  3. 占据内存的地址就是地址值最小的那个字节的地址

现在就可以理解 a 在内存中为什么占4个字节,而且首地址为0028FF40了。

3、指针对象(变量)

用来保存指针的对象,就是指针对象。如果指针变量p1保存了变量 a 的地址,则就说:p1指向了变量a,也可以说p1指向了a所在的内存块 ,这种指向关系,在图中一般用 箭头表示:
在这里插入图片描述
指针对象p1,也有自己的内存空间,32位机器占4个字节,64位机器占8个字节。所以会有指针的指针。

3.1、定义指针对象

定义指针变量时,在变量名前写一个 * 星号,这个变量就变成了对应变量类型的指针变量。必要时要加( ) 来避免优先级的问题:

int* p_int; 		//指向int类型变量的指针         

double* p_double; 	//指向double类型变量的指针  
   
Student* p_struct; 	//类或结构体类型的指针

int** p_pointer; 	//指向 一个整形变量指针的指针

int(*p_arr)[3]; 	//指向含有3个int元素的数组的指针 
 
int(*p_func)(int,int); 	//指向返回类型为int,有2个int形参的函数的指针  

3.2、获取对象地址

指针用于存放某个对象的地址,要想获取该地址,虚使用取地址符(&),如下:

int add(int a , int b)
{
    return a + b;
}

int main(void)
{
    int num = 97;
    float score = 10.00F;
    int arr[3] = {1,2,3};
    
    int* p_num = #
    int* p_arr1 = arr;		//p_arr1意思是指向数组第一个元素的指针
    float* p_score = &score;
    int (*p_arr)[3] = &arr;           
    int (*fp_add)(int ,int )  = add;  //p_add是指向函数add的函数指针
    const char* p_msg = "Hello world";//p_msg是指向字符数组的指针
    return 0;
}

通过上面可以看到&的使用,但是有几个例子没有使用&,因为这是特殊情况:

  1. 数组名的值就是这个数组的第一个元素的地址
  2. 函数名的值就是这个函数的地址
  3. 字符串字面值常量作为右值时,就是这个字符串对应的字符数组的名称,也就是这个字符串在内存中的地址

3.3、解析地址对象

如果指针指向了一个对象,则允许使用解引用符(*)来访问该对象,如下:

int 	age = 19;
int*	p_age = &age;
*p_age  = 20;  			//通过指针修改指向的内存数据

printf("age = %d\n",*p_age);   	//通过指针读取指向的内存数据
printf("age = %d\n",age);

对于结构体和类,两者的差别很小,所以几乎可以等同,则使用->符号访问内部成员:

struct Student
{
    char name[31];
    int age;
    float score;
};

int main(void)
{
    Student stu = {"Bob" , 19, 98.0};
    Student*	p_s = &stu;

    p_s->age = 20;
    p_s->score = 99.0;
    printf("name:%s age:%d\n",p_s->name,p_s->age);
    return 0;
}

3.4、指针值的状态

指针的值(即地址)总会是下列四种状态之一

  1. 指向一个对象的地址
  2. 指向紧邻对象所占空间的下一个位置
  3. 空指针,意味着指针没有指向任何对象
  4. 无效指针(野指针),上述情况之外的其他值

第一种状态很好理解就不说明了,第二种状态主要用于迭代器和指针的计算,后面介绍指针的计算,迭代器等整理模板的时候在介绍。

空指针:在C语言中,我们让指针变量赋值为NULL表示一个空指针,而C语言中,NULL实质是 ((void*)0) , 在C++中,NULL实质是0。C++中也可以使用C11标准中的nullpte字面值赋值,意思是一样的。
任何程序数据都不会存储在地址为0的内存块中,它是被操作系统预留的内存块

无效指针:指针变量的值是NULL,或者未知的地址值,或者是当前应用程序不可访问的地址值,这样的指针就是无效指针,不能对他们做解指针操作,否则程序会出现运行时错误,导致程序意外终止。

任何一个指针变量在做解地址操作前,都必须保证它指向的是有效的,可用的内存块,否则就会出错。坏指针是造成C语言Bug的最频繁的原因之一。

未经初始化的指针就是个无效指针,所以在定义指针变量的时候一定要进行初始化如果实在是不知道指针的指向,则使用nullptr或NULL进行赋值

3.5、指针之间的赋值

指针赋值和int变量赋值一样,就是将地址的值拷贝给另外一个。指针之间的赋值是一种浅拷贝,是在多个编程单元之间共享内存数据的高效的方法。

int* p1  = &a;
int* p2 = p1;

在这里插入图片描述
p1和p2所在的内存地址不同,但是所指向的内存地址相同,都是0028FF40。

4、指针内含信息

通过上面的介绍,我们可以看出指针包含两部分信息:所指向的值和类型信息。

指针的值:这个就不说了,上面大部分都是这个介绍。
指针的类型信息:类型信息决定了这个指针指向的内存的字节数并如何解释这些字节信息。一般指针变量的类型要和它指向的数据的类型匹配。

同样的地址,因为指针的类型不同,对它指向的内存的解释就不同,得到的就是不同的数据

char array1[20] = "abcdefghijklmnopqrs";
char* ptr1 = array1;
int* ptr2 = (int*)ptr1;

ptr1++;
ptr2++;
cout << &array1 << endl;
cout << *ptr1 << endl;
cout << ptr2 << endl;
cout << *(char*)ptr2 << endl;

运行结果如下:
在这里插入图片描述
这里&array1,是数组的地址,其实和数组首地址&array1[0]是一样的。
ptr1和ptr2都指向了同一块地址,但是ptr1的类型个ptr2的类型不一致,导致内存解释不一样,所以同样是后++操作,但是结果却不一样。

上面我用了C的强制转换,C++中有另一套转换方法。其根本,转换(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出内存的大小和其内容”的解释方式

4.1、void*指针

void* 指针是一种特殊的指针类型,可用于存放任意对象的地址,但是丢失了类型信息。如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。因为,编译器不允许直接对void*类型的指针做解指针操作(提示非法的间接寻址)。

利用void所做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void对象

5、指针的算数运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,指针的运算是有单位的

如上面第4节介绍的例子:
ptr1和ptr1都被初始化为数组的地址,并且后续都加1。
指针ptr1的类型是char*,它指向的类型是char,ptr1加1,编译器在1的后面乘上了单位sizeof(char)。
同理ptr2的类型是int*,它指向的类型是int,ptr2加1,编译之后乘上了单位sizeof(int)。
所以两者的地址不一样,通过打印信息,可以看出两者差了2个字符。

若ptr2+3,则编译之后的地址应该是 ptr2的地址加 3 * sizeof(int),打印出的字母应该是m

指针运算最终会变为内存地址的元素,内存又是一个连续空间,所以按理只要没有超出内存限制就可以一直增加。这样前面所说的指针值的状态第二条就很好解释了。

6、函数和指针

6.1、函数的参数和指针

实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。

void change(int a)
{
    a++;      //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。
}

int main(void)
{
    int age = 19;
    change(age);
    printf("age = %d\n",age);   // age = 19
    return 0;
}

有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。

传递变量的指针可以轻松解决上述问题。

void change(int* pa)
{
    (*pa)++;   //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时
               //会直接去内存中找到age这个数据,然后把它增1。
}
int main(void)
{
    int age = 19;
    change(&age);
    printf("age = %d\n",age);   // age = 20
    return 0;
}

上述方法,当然也可以用引用的方式。之后会整理引用和指针的区别。

传递指针还有另外一个原因:
有时我们会传递类或者结构体对象,而类或者结构体占用的内存有时会比较大,通过值传递的方式会拷贝完整的数据,降低程序的效率。而指针只是固定大小的空间,效率比较高。当然如果你用C++,使用引用效率比指针更高。

6.2、函数的指针

每一个函数本身也是一种程序数据,一个函数包含了多条执行语句,它被编译后,实质上是多条机器指令的合集。在程序载入到内存后,函数的机器指令存放在一个特定的逻辑区域:代码区。既然是存放在内存中,那么函数也是有自己的指针的。

其实函数名单独进行使用时就是这个函数的指针。

int add(int a,int b)		//函数的定义
{
	return a + b;
}

int (*function)(int,int); 	//函数指针的声明

function = add;		//给函数指针赋值
function = &add;		//跟上面是一样的

int c = function(1,2);	 	//跟函数名一样使用
int d = (*function)(1,2);	//跟上面的调用是一样的

6.3、返回值和指针

这里唯一需要注意的是不要把非静态局部变量的地址返回。我们知道局部变量是在栈中的,由系统创建和销毁,返回之后的地址有可能有效也有可能无效,这样会造成bug。

可以返回全局变量、静态的局部变量、动态内存等的地址返回。

7、const与指针

这里主要的就是指针常量和常量指针了,两者的区别是看const修饰的谁。

7.1、常量指针

实际是个指针,指针本身是个常量。

int	a = 97;
int	b = 98;
int* const p = &a;
*p 	= 98;		//正确
p 	= &b;		//编译出错

常量指针必须初始化,而且一旦初始化完成,则它的值就不能改变了

7.2、指向常量的指针

int a = 97;
int b = 98;       
const int* p = &a;
int const *p = &a; 	//两者的含义是一样的
*p = 98;		//编译出错
p = &b;			//正确

所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,但是对象的值可以通过其它途径进行改变。

需要注意的是常量变量,必须使用指向常量的指针来指向。但是对于非常量变量,两者都可以。

8、浅拷贝和深拷贝

如果2个程序单元(例如2个函数)是通过拷贝 他们所共享的数据的 指针来工作的,这就是浅拷贝,因为真正要访问的数据并没有被拷贝。如果被访问的数据被拷贝了,在每个单元中都有自己的一份,对目标数据的操作相互 不受影响,则叫做深拷贝
在这里插入图片描述

9、指针和数组

这个之前整理数组的时候整理过了,大家可以看数组的详解

10、比较经典面试题

1、

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}

void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str,"hello world");
	printf(str);
}
运行的结果是什么?

2、

int array[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
int *p = array;
p += 5;
int* q = NULL;
*q = *(p+5);
printf("%d %d",*p,*q);
运行结果是什么?

3、

int arr[5] = {0,1,2,3,4};
const int *p1 = arr;
int* const p2 = arr;
*p1++;
*p2 = 5;
printf("%d,%d",*p1,*p2);
*p1++ = 6;
*p2++ = 7;
printf("%d %d \n", *p1, *p2);

第一道题:我们一看函数传递,指针只是浅拷贝,申请的内存在临时对象p中,并没有传递到函数外面,然后又对str地址进行写操作,str初始地址为NULL,不能进行书写,所以系统会崩溃。

第二道题:一看很开心是指针类型的加减法,下标从0开始,但是数字从1开始,所以应该是6 11。但是你忽略了q是一个NULL指针,不能进行书写,所以会崩溃。

第三道题:指针指向数组,数组退化成指针,前两个指针操作是对的。但是后面*p1++ = 6; 不可以通过p1进行值的修改,*p2++ = 7;不能对p2进行修改。所以这道题是编译出错。

感谢大家,我是假装很努力的YoungYangD(小羊)。

参考资料:
《C++ primer 第五版》
http://www.cnblogs.com/lulipro/p/7460206.html
https://www.cnblogs.com/ggjucheng/archive/2011/12/13/2286391.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++指针详解 的相关文章

随机推荐

  • c语言 get post,简述GET和POST的区别

    表面的区别 1 GET在浏览器回退时是无害的 xff0c 而POST会再次提交请求 2 GET产生的URL地址可以被Bookmark xff0c 而POST不可以 3 GET请求会被浏览器主动cache xff0c 而POST不会 xff0
  • arduino的串口缓冲区_arduino串口通信

    1 串口设置 arduino串口通信 Serial begin 说明 开启串口 xff0c 通常置于setup 函数中 语法 Serial begin speed Serial begin speed xff0c config 参数 spe
  • linux映像文件结构,ROMFS文件系统分析[三]ROMFS映像文件结构

    前面说了我们使用genromfs工具可以生成romfs文件系统 xff0c 那其生成的映像的格式是什么 xff1f 这就要探究romfs文件系统的本质了 1 romfs映像结构使用genromfs生成的romfs格式映像中 xff0c 文件
  • 依据imu姿态角计算z轴倾角_树莓派小车-07-IMU姿态解算 imu_complementray_filter

    上一篇文章介绍了互补滤波器与ROS的接口定义 xff0c 这篇文章将结合论文分析代码 complementary filter cpp 首先从成员变量开始看 xff0c 毕竟这些变量在后面用到的时候需要了解他所代表的意思 xff0c 同时也
  • android 动画 三色流动,Android 三色状态指示进度条 - ThreeColorIndicator

    ThreeColorIndicator 这是一个 Android 三色状态指示进度条 xff0c 常用于指示 xff1a 信号强度 温度等 xff0c 可通过文字 颜色表示一个值的好 一般 差 xff0c 也可以自定义为其它状态 预览图 使
  • stm32串口接收不定长数据_串口发送数据的验证 不定长度多字节的处理4

    最后遗留的一个问题 xff0c 在Modbus RTU的读取功能中就完美解决了 灵光一闪 发送帧的第5字节数据就要要读取的长度 xff0c 响应帧的第3字节数据就是返回数据的长度 xff0c 后面的字节就是返回数据 因为1个寄存器数据是2个
  • m3u直播源_M3U播放列表生成工具

    来源 xff1a 黑鸟博客 快速制作支持 VLC 和 Potplayer 等播放器的 XSPF DPL M3U 等播放列表格式的工具 xff0c 并且可以查重复 xff0c 自定义设置多种选项 所有的播放列表都可以使用普通的文本编辑器 xf
  • js字符串包含另一个字符串_C语言中,在一个字符串中插入另一个字符串(字符)...

    本题可以看做一个字符串拼接问题 需要一个载体数组 includevoid insert array char s1 char s2 int n 思路 1 得到主串s1和子串s22 找到插入位置 3 进行插入 void main char s
  • 华为手机一键解锁工具箱下载 | 华为手机解BL锁软件: 支持解锁bootloader,刷写recovery功能

    文章目录 1 软件介绍2 特色功能3 资源站点4 下载地址5 软件截图6 安装教程7 使用教程7 1 解锁BL 1 软件介绍 通过这款华为手机实用工具箱可以对你的华为手机系列进行刷机 解锁等操作 xff0c 网上这种华为刷机解锁工具比较少
  • python subprocess 实时输出_Python标准库初探之subprocess

    一 subprocess简介 人生苦短 xff0c 我用Python 今天给大家带来一个在Python脚本中启动进程的利器 subprocess 人们都说Python是一个胶水语言 xff0c 可以方便地在多平台上调用其他指令 xff0c
  • 进程内存中堆和栈的区别

    1 概述 在整理数据结构时 xff0c 整理过栈 队列和堆 xff0c 但是在学习进程分布的时候又碰到了 栈和堆 xff0c 初学时很容易把这几个概念给弄混 xff0c 今天有空就给整理一下 2 程序在内存中的分布 程序在内存中的分布如下图
  • C++ Mutable

    1 mutable 含义及常规使用 mutable 英文中表示 xff0c 易变的 xff0c 不定的 xff1b 性情不定的 xff0c 而在代码中表示 可变数据成员 由前面整理的 const详解 知道 xff0c 由const修饰的成员
  • 牛吃草问题

    1 概述 最近碰到一个面试题 xff0c 讲的是牛吃草的问题 xff0c 当时时间短 xff0c 脑袋出现了短路 xff0c 没有给出答案 回来特意查了一下答案 xff0c 发现了一篇比较好的文章 xff0c 现在重新抄写一份 xff0c
  • 开始记录学习中的点滴

    随着年龄的增长 xff0c 除了去了很多地方之外 xff0c 感觉个人没有特别明显的成长 xff0c 对于未来充满了更多的迷茫与困惑 对于程序员的我来说更是感觉到了自己的瓶颈 xff0c 知识储备没有增加多少 xff0c 随着时间的流逝 x
  • C++中Struct与Class的区别与比较

    概述 之前只知道在C 43 43 中类和结构体的区别只有默认的防控属性 xff08 访问控制 xff09 不同 xff0c struct是public的 xff0c 而class是private的 但经过上网查资料才发现 xff0c 除了这
  • 函数调用约定的详解

    概述 在工作的过程中 xff0c 我们总是需要调用底层函数或者使用第三方的库 xff0c 在使用的过程中我就发现了有一些函数前面总有一些 stdcall xff0c 之初我只知道那是调用约定 xff0c 但别人问我什么是调用约定 xff0c
  • #pragma的常用方法讲解

    概述 我们在写代码时 xff0c 总会遇到头文件多次包含的情况 xff0c 刚开始时我们使用宏定义进行控制 xff0c 之后发现有 pragma once这样简单的东西 xff0c 当时是很兴奋 xff0c 以为 pragma就这一种用法
  • C++数组的详细解析

    概述 数组在写程序时经常用到 xff0c 但是对于它和指针的关系 xff0c 自己经常搞混 xff0c 所有抽点时间对数组进行整理 1 数组的概念和使用 数组是用来存储相同类型的变量的顺序集合 所有的数组都是由连续的内存位置组成 最低的地址
  • 华为荣耀9升降级系统 | 华为荣耀9变砖后如何救砖 | 华为荣耀9获取BL解锁码以及如何解BL锁 | 华为荣耀9如何通过写ramdisk.img来获取root

    文章目录 1 按2 通过官方华为手机助手升降级以及修复系统和安装驱动3 使用百分之五模式刷高维禁用包355来安装指定的系统版本8 0 0 3554 故意 xff08 或意外 xff09 刷错包把手机变砖5 使用救砖模式刷高维禁用包355来安
  • C++指针详解

    概述 C C 43 43 语言之所以强大 xff0c 以及其自由性 xff0c 很大部分体现在其灵活的指针运用上 因此 xff0c 说指针是C C 43 43 语言的灵魂一点都不为过 有好的一面 xff0c 必然会有坏的一面 xff0c 指