突破编程_C++_基础教程(指针)

news/2024/12/22 15:11:01/

1 指针的基础概念

指针是 C++ 的核心之一,使用 C++ 语言构建的程序之所以性能强悍,有很大部分原因是体现在使用指针直接操作内存。当然这样的工具是一把双刃剑,错误的指针操作可能会导致程序崩溃或者数据损坏。
指针主要有四个方面的用途:
(1)动态内存分配:使用 new 操作符在堆上分配内存。
(2)传递数据:通过指针传递大型数据对象可以显著提高程序的效率(比如使用指针作为函数参数)。
(3)回调函数:指针可以用于传递函数的地址,函数式编程正是建立在这个功能的基础上。
(4)优化性能:指针可以直接访问内存,避免了一些额外的开销,如复制数据或者查找数据等。
指针本身是一个变量,其值为另一个变量的内存地址,因此,要掌握指针的原理与作用,需要从理解内存地址开始。

1.1 内存地址

内存地址是指计算机内存中存储变量或对象的地址。内存空间大小就是寻址能力,即能访问到多少个地址,比如 32 位机器内存空间大小就是 2^32 = 4294967296,也就是 4 GB 。每个变量或对象在内存中都有一个唯一的地址,通过该地址可以访问和操作该变量或对象。注意一 个内存地址对应一个字节,以 int 类型的变量为例,其占据 4 个内存地址,其中首个内存地址就是这个变量的地址。

#include <iostream>int main()
{int vals[4]{};printf("val1 address = %p\n", &vals[0]);printf("val2 address = %p\n", &vals[1]);printf("val3 address = %p\n", &vals[2]);printf("val4 address = %p\n", &vals[3]);return 0;
}

上面代码的输出为:

val1 address = 0000005420F6F978
val2 address = 0000005420F6F97C
val3 address = 0000005420F6F980
val4 address = 0000005420F6F984

为了能够说明 1 个 int 类型的变量占据 4 个内存地址,我们在上面的代码中使用占据连续内存的数组来做测试,由这个输出可以看出:数组 vals 的第一个元素所占据的内存地址由 0000005420F6F9780000005420F6F97B (再往下的一个地址就是第二个元素的首地址 0000005420F6F97C),刚好是 4 个内存地址,其首个内存地址 0000005420F6F978 就是这个数组 vals 的第一个元素的地址(同时也是这个数组变量 vals 的地址)。

1.2 指针是什么

指针是一种变量,它存储的是其他变量的内存地址。通过指针,我们可以间接地访问和操作存储在内存中的变量。
由这个定义可知,指针既然是一个变量,那么它本身也需要占用内存,即有自己对应的内存地址。如下为样例代码( x64 平台编译):

#include <iostream>int main()
{void* ptr = nullptr;printf("ptr address = %p\n", &ptr);printf("ptr address size = %llu\n", sizeof(ptr));printf("ptr value = %p\n", ptr);return 0;
}

上面代码的输出为:

ptr address = 000000196B5CF678
ptr address size = 8
ptr value = 0000000000000000

其中,指针 ptr 虽然指向的是一个空地址,但是其作为一个变量,依然有自己的内存地址(000000788E9AF638)。另外,ptr address size = 8 表明使用 x64 平台编译时,指针所占用的内存大小为 8 个字节( 32 位平台编译是 4 个字节),刚好可以保存一个内存地址,这就是指针能够存储其他变量的内存地址的原理。

2 指针的基本使用

指针是一种变量,所以在使用前和其他类型变量一样,也需要定义与初始化:指针变量定义时前面会有一个星号(*)。例如,int *ptr; 意思是定义了一个指向整数的指针。指针变量在使用之前必须被初始化,否则其值是未定义的,这个时候指向的是一个随机的内存地址,对其操作很容易引起程序崩溃。通过在指针变量前加上星号(*)可以访问指针所指向的对象,相当于操作这个对象本体。

2.1 指针的定义与初始化

指针的定义和初始化可以通过以下方式完成:

#include <iostream>int main()
{int val = 1;			// 定义一个整型变量 val,并初始化为 1  int *ptr = &val;		// 定义一个指向整型的指针 ptr,并将它初始化为变量 val 的地址printf("val address = %p\n", &val);printf("ptr address = %p\n", &ptr);printf("ptr value = %p\n", ptr);return 0;
}

上面代码的输出为:

val address = 00000035076FFCB4
ptr address = 00000035076FFCD8
ptr value = 00000035076FFCB4

其中,指针 ptr 的值等于整型变量 val 的地址。第 6 行 int *ptr = &val; 中的 & 符号是取地址符,用于获取变量的内存地址。基本类型( int 、float 等)、结构体类型( struct )以及类类型( class )的地址获取都需要使用该符号。

2.2 解引用

*操作符是 C++ 的解引用操作符,用于获取指针所指向的对象,对其操作相当于对指针所指向对象的操作:
指针的定义和初始化可以通过以下方式完成:

#include <iostream>int main()
{int val1 = 1;	int *ptr = &val1;	*ptr = 2;					//该表达式相当于 val1 = 2;	int val2 = *ptr;			//该表达式相当于 int val2 = val1;	printf("val1 = %d\n", val1);printf("val2 = %d\n", val2);return 0;
}

上面代码的输出为:

val1 = 2
val2 = 2

其中,*ptr 在上面程序的运行过程中就是整型变量 val1 ,不管是对其做赋值操作(*ptr = 2;),还是将其用于其他变量的初始化(int val2 = *ptr;),都相当于直接操作整型变量 val1 自身。 对于结构体和类,一般是使用箭头操作符 -> 来操作对象的成员变量或者成员函数,但是根据前面所描述的解引用概念,使用解引用操作符也可以起到相同作用:

#include <iostream>
#include <string>using namespace std;class Student
{
public:Student() {};Student(string name):m_name(name) {};~Student() {};public:string getName(){return m_name;}private:string m_name;
};int main()
{Student st("zhangsan");Student *ptr = &st;string name1 = st.getName();string name2 = ptr->getName();string name3 = (*ptr).getName();		//使用解引用操作符return 0;
}

注意上面的语句 string name3 = (*ptr).getName();,同样可以调用对象 st 的成员函数。只是由于 C++ 提供了更方便的箭头操作符 -> ,所以一般我们才不会如此使用。

2.3 指向数组的指针

指向数组与上面章节的指向基本类型( int 、float 等)、结构体类型( struct )以及类类型( class )的使用方式有所不同,数组名本身就是数组的首地址,所以无需做取地址操作,如下为样例代码:

#include <iostream>int main()
{int vals[6] = { 1,2,3,4,5,6 };printf("%p \n", vals);printf("%p \n", &vals);int* ptr = vals;for (size_t i = 0; i < 6; i++){printf("%d ", *(ptr+i));}return 0;
}

上面代码的输出为:

vals address = 000000C03976F8C8
&vals address = 000000C03976F8C8
1 2 3 4 5 6

从上面输出可以看出,数组名 vals 与对数组名取地址 &vals 所得到的内存地址是一样的,所以如果用指针指向某个数组,直接将数组名赋值给指针即可。第 13 行 printf("%d ", *(ptr+i)); 中的 *(ptr+i) 是指针的运算,在下面章节会详细讲解。
指针不光可以指向整个数组,还可以指向数组中的某一个元素,如下:

#include <iostream>int main()
{int vals[6] = { 1,2,3,4,5,6 };printf("before modification, vals[2] = %d \n", vals[2]);int* ptr = &vals[2];			//指向的数组第 3 个元素*ptr = 10;		//将其所指向的数组第 3 个元素的值修改为 10printf("after modification, vals[2] = %d \n", vals[2]);return 0;
}

上面代码的输出为:

before modification, vals[2] = 3
after modification, vals[2] = 10

注意第 10 行 int* ptr = &vals[2]; 这里是指向数组里面的一个整型元素,所以一定要用取地址操作符。

2.4 指向函数的指针

C++中的函数也有地址(调用函数的本质就是跳转到这个函数的地址,然后执行里面的函数体)。因此,可以声明指向函数的指针,并使用这个指针调用函数。指向函数的指针也被称作是函数指针,其定义方式为:

函数返回值类型 (`*` 指针变量名) (函数参数列表);

函数返回值类型:表示该指针变量所指向函数的返回值类型。
指针变量名:表示该指针变量的名称。
函数参数列表:表示该指针变量所指向函数的参数列表。
为了使用方便,一般会用关键字 typedef 来定义函数指针,即:typedef 函数返回值类型 (* 指针变量名) (函数参数列表) 。例如:

typedef int (*ADD)(int,int);
ADD addFunc;

使用这种方式可以目标函数看作为一个类型,然后再用它去定义指针,增强复用性。
对于无参数或者无返回值的函数,需要使用用 void 关键字,例如:

typedef void (*TESTFUNC)(void); 	//无参数和返回值

2.4.1 指向全局函数的函数指针

以如下代码为例:

#include <iostream>int add(int a, int b)
{int sum = a + b;return sum;
}int main()
{typedef int(*ADDFUNC)(int, int);ADDFUNC f1 = add;int sum1 = f1(1, 2);			//直接使用函数名int sum2 = (*f1)(1, 2);			//取函数地址printf("sum1 = %d\n",sum1);printf("sum2 = %d\n", sum2);return 0;
}

上面代码的输出为:

sum1 = 3
sum2 = 3

特别注意的是,因为函数名本身就可以表示该函数地址(指针),因此在获取函数指针时,可以直接用函数名,也可以取函数的地址。因此,上面代码中 int sum1 = f1(1, 2); 以及 int sum2 = (*f1)(1, 2); 作用是相同的。

2.4.2 指向对象成员函数的函数指针

以如下代码为例:

#include <iostream>class MyAdd 
{
public:MyAdd() {}~MyAdd() {}public:int add(int a, int b){int sum = a + b;return sum;}};int main()
{MyAdd myAddObj;typedef int(MyAdd::*ADDFUNC)(int, int);ADDFUNC f1 = &MyAdd::add;int sum = (myAddObj.*f1)(1, 2);printf("sum = %d\n", sum);return 0;
}

上面代码的输出为:

sum = 3

注意:对象的成员函数属于类,所以其存储位置在对象外的空间中,由所有的类对象共享。因此, MyAdd 类中的 add() 成员函数,不是属于 myAddObj 对象的,而是属于 MyAdd 类。所以使用 &类名::成员函数名 的形式将该成员函数赋给函数指针。

2.4.3 回调函数

回调函数是函数指针的一个重要应用场景,比如在使用 C++ 的容器类时,经常会自定义回调函数用以实现定制化功能。以 vector 的自定义排序为例,代码如下:

#include <iostream>
#include <vector>
#include <algorithm>using namespace std;struct Student 
{string id;double score;
};bool compareByScore(Student& stu1, Student& stu2)
{return stu1.score < stu2.score;
}int main()
{vector<Student> students;students.emplace_back(Student{ "s1",98.2 });students.emplace_back(Student{ "s2",97.6 });students.emplace_back(Student{ "s3",92.8 });students.emplace_back(Student{ "s4",95 });students.emplace_back(Student{ "s5",99 });printf("before sort\n");for (size_t i = 0; i < students.size(); i++){printf("%s(%lf) ", students[i].id.c_str(), students[i].score);}printf("\n");sort(students.begin(), students.end(), compareByScore);printf("after sort\n");for (size_t i = 0; i < students.size(); i++){printf("%s(%lf) ", students[i].id.c_str(), students[i].score);}printf("\n");return 0;
}

上面代码的输出为:

before sort
s1(98.200000) s2(97.600000) s3(92.800000) s4(95.000000) s5(99.000000)
after sort
s3(92.800000) s4(95.000000) s2(97.600000) s1(98.200000) s5(99.000000)

其中,函数 compareByScore 便作为一个函数指针的入参传递给函数 sort

2.4.4 函数指针和指针函数的区别

函数指针和指针函数是两种不同的编程概念,前者是一个指针,后者是一个函数,除了名字比较容易混淆,实际上是完全不同的概念。
上面内容已经说明了函数指针的含义与作用,指针函数的定义如下:
(1)指针函数本身就是一个函数,其返回的类型是指针。
(2)指针函数用于返回指针类型的值,例如动态分配的对象或数组的指针。

2.5 指向指针的指针

指针可以指向所有数据类型的变量(基本类型、结构体类型、类类型等),而指针自身也是一种变量,所以指针自然也可以指向指针。把指向指针的指针理解透彻,基本上也就能掌握了指针的精髓。如下为样例代码:

#include <iostream>int main()
{int val1 = 1;int *ptr1 = &val1;int **ptr2 = &ptr1;printf("ptr1 address = %p\n", &ptr1);printf("ptr1 address = %p\n", &(*ptr2));printf("ptr2 value = %p\n", ptr2);return 0;
}

上面代码的输出为:

ptr1 address = 000000C4A839F758
ptr1 address = 000000C4A839F758
ptr2 value = 000000C4A839F758

由结果可以看出,指向指针的指针变量 ptr2 保存了指针变量 ptr1的地址( 000000C4A839F758 )。 其中代码第 10 行 int **ptr2 = &ptr1; 定义了一个指向指针的指针,这里用了两个星号*,其保存的值就是指针变量 ptr1的地址。
第 11、 12、 13 行代码尤为重要:
第 11 行代码 printf("ptr1 address = %p\n", &ptr1); ,其中的 &ptr1 是对指针变量 ptr1 做取地址操作。
第 12 行代码 printf("ptr1 address = %p\n", &(*ptr2)); ,其中的 (*ptr2) 是对指针变量 ptr2 做解引用操作,再对其做取地址操作,相当于直接对指针变量 ptr1 做取地址操作。
第 13 行代码 printf("ptr2 value = %p\n", ptr2); ,对指向指针的指针取值,直接用其变量名即可。

2.6 创建动态内存

使用指针可以在堆中创建内存空间(先在堆中申请一块内存空间,然后将其首地址返回给一个指针,后面通过该指针便可读写这一块内存),其创建和销毁过程都需要手动控制。 C++ 使用 new 或者 new[] 操作符在堆中创建一块内存空间,使用 delete 或者 delete[] 释放这块申请的内存空间。如下:

int* ptr = new int;

这一行代码定义了一个指向整型的指针变量 ptr ,并且使用 new 操作符在堆中创建一个 int 类型的内存空间,并将该空间首地址返回指针变量 ptr
如果想将这个内存空间赋值为 1 ,可以做如下操作:

*ptr = 1;

在使用完这个内存空间后,一定要将其释放(避免内存泄露),并且将指针变量 ptr 赋值 nullptr(避免悬垂指针,它所指向的内存空间已经被释放):

delete ptr;
ptr = nullptr;

注意释放的操作不能再次执行,如果再做一次 delete ptr; 则会导致程序崩溃。

2.7 指针的运算

指针为什么一定要定义类型(即使无类型,也需要使用 void 做定义),这个要求的一个来源就是指针运算需要按照类型做处理:

#include <iostream>int main()
{int val1 = 1;short val2 = 2;int *ptr1 = &val1;short *ptr2 = &val2;printf("before adding, ptr1 value = %p\n", ptr1);printf("before adding, ptr2 value = %p\n", ptr2);ptr1++;ptr2++;printf("after adding, ptr1 value = %p\n", ptr1);printf("after adding, ptr2 value = %p\n", ptr2);return 0;
}

上面代码的输出为:

before adding, ptr1 value = 0000000BBC54F614
before adding, ptr2 value = 0000000BBC54F634
after adding, ptr1 value = 0000000BBC54F618
after adding, ptr2 value = 0000000BBC54F636

从上面代码运行的结果可以看出:不同类型的指针变量,其运算的步长由其类型确定。 int 类型的指针变量,对其做 ++ 操作后,该变量的值增加了 4 ,指向下一个 int 变量。 short 类型的指针变量,对其做 ++ 操作后,该变量的值增加了 2 ,指向下一个 short 变量。

2.7.1 指针的加减运算

指针的加减运算通常用于对数组的操作,如下为样例代码:

#include <iostream>int main()
{int vals[6] = { 1,2,3,4,5,6 };int* ptr = vals;for (size_t i = 0; i < 6; i++){printf("%d ", *(ptr + i));}return 0;
}

上面代码的输出为:

1 2 3 4 5 6

上面代码的核心语句是 printf("%d ", *(ptr + i));,其中 ptr + i 是指向数组中的第 i 个元素的地址,再加上前面的星号 * ,则完成了对其的解引用操作,最终获取到了对应数组元素的值。

2.7.2 指针的赋值操作

指针的赋值操作也是一个在开发中常见的操作。其作用是将一个指针的值(这个值是内存中某一个变量的地址)赋给另一个指针。如下为样例代码:

#include <iostream>int main()
{int val1 = 1;int *ptr1 = &val1;int *ptr2 = ptr1;printf("ptr1 value = %p\n", ptr1);printf("ptr2 value = %p\n", ptr2);return 0;
}

上面代码的输出为:

ptr1 value = 0000006FCF3CFBC4
ptr2 value = 0000006FCF3CFBC4

赋值操作后, 指针变量 ptr2 的值就等于指针变量 ptr1 的值。

3 使用指针的注意点

3.1 常量指针与指针常量

常量指针(const pointer)和指针常量(pointer to const)是两个不同的概念,常量指针指的是其指向变量的值不可改变,但是指针本身是可以改变的,可以指向其他变量;指针常量指的是指针本身是常量,其不可以再指向其他变量。
常量指针的样例代码:

const int val1 = 1;
int *ptr1 = &val1;			//错误:必须使用常量指针
const int *ptr1 = &val1;	//OK
*ptr1 = 2;	

指针常量的样例代码:

int val1 = 1;
int val2 = 2;
int const *ptr1 = &val1;	//OK
*ptr1 = &val2;				//错误:指针本身是常量,其不可以再指向其他变量。

3.2 使用 nullptr

前面章节的代码中,多处使用了 nullptr 关键字,该关键字是在 C++11 标准中引入的,用于表示空指针。在 C++11 及以后的版本中,nullptr 替代了 C++98/03 中的 NULL 或 0 作为空指针的表示。该关键字可以避免函数重载问题,如下为样例代码:

void overLoadFunc(int* val);
void overLoadFunc(int val);int main()
{overLoadFunc( NULL );  // 期待调用 overLoadFunc(int* val); 但实际调用却是 overLoadFunc(int val);
}

上面代码中的 overLoadFunc( NULL ); 实际调用的是 overLoadFunc(int val); 。其原因是 NULL 本身就是整数 0 ,因此进入了整型参数的重载函数。

3.2 野指针出现的原因

野指针出现的原因主要有以下三种:
(1)指针变量未初始化。局部指针变量的默认值是一个随机值,如果此时访问该指针则会引起程序崩溃。所以,指针变量在创建的同时应当被初始化,要么将指针设置为 nullptr ,要么让它指向合法的内存( new 出来的对象或者现有的一个对象)。
(2)释放内存后没有将指针设置为 nullptr 。不管是 free 还是 delete 在释放内存时,只是把指针所指的内存给释放掉了,但此时指针的值依然是之前内存空间的首地址。此时访问该指针则会引起程序崩溃。
(3)指针操作超越变量作用范围。栈内存在函数结束时会被释放,如果将其内存地址通过指针返回给调用者,此时再访问则会引起程序崩溃。


http://www.ppmy.cn/news/1346697.html

相关文章

又一款图像AI应用爆火,团队仅两人,单月吸引40万用户

又一款AI原生应用火了。近日&#xff0c;AI图像增强应用Magnific AI广受追捧&#xff0c;该应用在发布一个多月之后吸引了40万注册用户。 Magnific AI不仅可以用生成式AI技术放大图像&#xff0c;还能一键提升图像的分辨率&#xff0c;把原图呈现的更清晰&#xff0c;更有质感…

洛谷:P1219 [USACO1.5] 八皇后 Checker Challenge(dfs深度优先遍历求解)

题目描述 一个如下的 6666 的跳棋棋盘&#xff0c;有六个棋子被放置在棋盘上&#xff0c;使得每行、每列有且只有一个&#xff0c;每条对角线&#xff08;包括两条主对角线的所有平行线&#xff09;上至多有一个棋子。 上面的布局可以用序列 2 4 6 1 3 52 4 6 1 3 5 来描述&am…

LeetCode LCP 30.魔塔游戏:贪心(优先队列)

【LetMeFly】LCP 30.魔塔游戏&#xff1a;贪心&#xff08;优先队列&#xff09; 力扣题目链接&#xff1a;https://leetcode.cn/problems/p0NxJO/ 小扣当前位于魔塔游戏第一层&#xff0c;共有 N 个房间&#xff0c;编号为 0 ~ N-1。每个房间的补血道具/怪物对于血量影响记于…

用友U8+OA doUpload.jsp 文件上传漏洞复现

0x01 产品简介 用友U8+ OA经过20多年的市场锤炼,不断贴近客户需求,以全新UAP为平台,应对中型及成长型企业客户群的发展,提供的是一整套企业级数智化升级解决方案,为成长型企业构建精细管理、产业链协同、社交化运营为一体的企业互联网经营管理平台,助力企业应势而变,赢…

Python(21)正则表达式中的“元字符”

大家好&#xff01;我是码银&#x1f970; 欢迎关注&#x1f970;&#xff1a; CSDN&#xff1a;码银 公众号&#xff1a;码银学编程 获取资源&#xff1a;公众号回复“python资料” 在本篇文章中介绍的是正则表达式中一部分具有特殊意义的专用字符&#xff0c;也叫做“元…

重写Sylar基于协程的服务器(7、TcpServer HttpServer的设计与实现)

重写Sylar基于协程的服务器&#xff08;7、TcpServer & HttpServer的设计与实现&#xff09; 重写Sylar基于协程的服务器系列&#xff1a; 重写Sylar基于协程的服务器&#xff08;0、搭建开发环境以及项目框架 || 下载编译简化版Sylar&#xff09; 重写Sylar基于协程的服务…

两次NAT

两次NAT即Twice NAT&#xff0c;指源IP和目的IP同时转换&#xff0c;该技术应用于内部网络主机地址与外部网络上主机地址重叠的情况。 如图所示&#xff0c;两次NAT转换的过程如下: 内网Host A要访问地址重叠的外部网络Host B&#xff0c;Host A向位于外部网络的DNS服务器发送…

【网页设计期末】茶文化网站

本文资源&#xff1a;https://download.csdn.net/download/weixin_47040861/88818886 1.题目要求 设计要求&#xff1a; &#xff08;1&#xff09;网站页面数量不少于4个&#xff0c;文件命名规范&#xff0c;网站结构要求层次清楚&#xff0c;目录结构清晰&#xff0c;代码…