指针(pointer)是C/C++语言中的一种数据类型。指针与int、char等数据类型相似,都是在内存中开辟相应类型的数据区域使用,不同的是int存储的是整数值,而指针存储的是内存地址。指针是在内存中开辟指针类型的区域存储内存地址,通过指针存储的内存地址找到对应内存区域的值。简单讲就是通过一个内存地址找到另一个内存地址的值。虽然指针都是用来存储内存地址,但是在定义指针时要明确指针指向的内存区域的类型,比如int*
整数型指针指向整数型地址,char*
字符型指针指向字符型地址,void*
无类型指针指向任何类型地址,void*
指针指向的内容可以转换为任何类型的值。
一、什么是内存地址
指针是用来存储内存地址的数据类型,那么内存地址是什么?计算机软件在运行过程中使用内存进行数据存储,操作系统开辟的每一块内存区域都有相应的内存地址,每块内存区域又可以存储不同的数据,计算机通过内存地址来读取和写入内存区域的数据。当然开辟内存区域存储的内容为何类型,是整数还是字符串需要在定义时明确。
二、指针运算符
(一)运算符*
*
是单目运算符,其结合性为自右至左,用来表示指针变量所指的变量。在定义时表示定义和初始化,在定义和初始化后表示所指向的变量。
c
int myValue;
// 这里的*pint表示定义
int *pint = &myValue;
// 这里的*pint实际上就是myValue
*pint = 6;
(二)运算符&
&
是单目运算符,其结合性为自右至左,功能是取变量的地址。任何在内存中开辟的区域都可以通过&来获取地址,可以是变量、常量、指针。
c
// 变量地址
int myValue;
int *pint = &myValue;
// 指针地址
int **newPint = &pint;
// 赋值myValue为6
**newPint = 6;
// 常量地址
const int myValue1 = 10;
const int *pint1 = &myValue1;
内存区域:变量 内存地址 名称 类型 值 内存区域:一级指针 内存地址 名称 类型 值 内存区域:二级指针 内存地址 名称 类型 值 指向 指向 6 int myValue 0x325658 0x325658 int* pint 0x3848921 0x3848921 int** newPint 0x3256567
三、指针定义与初始化
指针与普通的常量变量一样,使用前需要先定义,意思是让操作系统开辟内存区域。指针定义后必须立即初始化,否则编译将会报错。初始化可以赋值某个内存地址,当不确定目标类型时可以初始化为空指针。
(一)初始化内存地址
c
int myValue;
int *pint = &myValue;
// 变量与指针同时定义
int myValue, *pint = &myValue;
指针指向的地址必须与目标地址的数据类型一致,如指针时int类型,指向的地址也必须时int类型,反之为非法赋值。
(二)初始化为常量
c
// 合法,表示pint指针指向一个无名字符串常量的地址,里面存放的数据是字符数组abc,
// 指针初始化为数组不需要&运算符,自动指向数组首地址。
char * pint = "abc";
// 非法
*pint = "def";
初始化指针时赋值字符串,表示在内存中开辟区域,并把首地址存储到pint指针中,指针指向地址为常量不可更改。
(三)初始化为数组
c
int ages[20];
int *pint = ages;
// 变量与指针同时定义
double readings[50], *marker = readings;
数组不需要&寻址符,赋值时将会把数组首地址赋值给指针。
指针指向数组项
c
int ages[20];
// 指针指向ages数组索引5的子项
int *pint = &ages[5];
指针指向数组项时需要使用&运算符。
(四)初始化空指针
1.空指针0
因为指针定义后必须赋值初始化,当未确定指针存放的地址时,可以赋值为0初始化为空指针。
c
int *ptrToint = 0;
double *ptrToDouble = 0;
0表示分配给当前未指向一个有效内存位置的指针,用户程序不能访问地址为 0 的内存,因为它被操作系统数据占用,这使得 0 成为指示无效内存位置值的安全选择。
2.空指针NULL
某些头文件(如iostream、fstream 、 cstdlib)定义了NULL常量为0地址,当程序包含这些头文件时只需要赋值NULL给指针即可定义空指针。
c
int *ptrToint = NULL;
float *ptrTofloat = NULL;
3.空指针nullptr
C++11 定义了关键字 nullptr 来指示一个无效的内存地址
c++
int *ptrToint = nullptr;
double *ptrToDouble = nullptr;
4.空指针的判断
指针在使用前可以先判断是否为空指针。
c
if (p != nullptr) { // 使用指针 P... }
if (p != NULL) { // 使用指针 P"... }
if (p != 0) { // 使用指针 P... }
// 等同于
if (p) { // 使用指针 P... }
四、指针的常量定义
指针的常量有指针常量、常量指针两种,区别在于定义时指针前有无修饰符const
。因为指针是内存地址的指向,两种常量涉及到指针本身和指向地址的修改关系,因此指针本身和指向地址到底哪个不可更改很容易混淆。可以理解为定义指针时在*
(指针)之后有const
为指针常量,之前则为常量指针。另外还有一种*
前后都有const
,表示指向常量的指针常量。
(一)指针常量
指针常量int * const
指针本身是常量不可变,指向地址可以是常量也可以是变量。
1.指向"变量"的指针常量
c
int myValue;
int newValue;
// 指向"变量"的指针常量,
int * const pint = &myValue;
// 非法
pint = &newValue;
// 合法
*pint = 6;
指针
int *
前无const
修饰符,指针指向地址为int类型,指针本身不可修改,指向的int类型变量可修改。
2.指向"常量"的指针常量
指向的地址是常量,"指针常量"前应加const
修饰符,即const int * const
。
c
const int myValue = 10;
const int newValue = 20;
// 指向"常量"的指针常量
const int * const pint = &myValue;
// 非法
pint = &newValue;
// 非法
*pint = 6;
指针
int *
前有const
修饰符,指针指向地址为const
修饰的int
类型,"指针常量"本身不可修改,指向的const
修饰的int
类型变量也不可修改。
(二)常量指针
常量指针const int *
指针本身是变量,指向的地址是常量。
c
const int myValue = 10;
const int newValue = 20;
// 指向"常量"的常量指针
const int * pint = &myValue;
// 或者
int const * pint = &myValue;
// 合法
pint = &newValue;
// 非法
*pint = 6;
因为定义指针是
const
修饰的指向类型,表示指向的地址不可更改。pint指针名前没有const修饰说明指针本身是变量,可以重新赋值。
五、多级指针
CC++可以使用多级指针指向同一块内存空间地址,也就是多层指针最终指向一个内存地址,修改任何多级指针的任何一级指针都可以修改指向地址的值。
c
#include <stdio.h>
int main() {
int myValue;
// 一级指针pint
int *pint0 = &myValue;
*pint0 = 5;
printf("Value:%d\n", myValue);
// 二级指针newPint
int **pint1 = &pint0;
**pint1 = 6; // 赋值myValue为6
printf("Value:%d\n", myValue);
// 三级指针newPint
int ***pint2 = &pint1;
***pint2 = 7; // 赋值myValue为6
printf("Value:%d\n", myValue);
return 0;
}
输出
cmd
Value:5
Value:6
Value:7
六、指针调用
C/C++函数是可以将指针作为参数定义的,在函数中定义的是形式参数,当调用该函数时将变量的地址传递给指针,函数中就可以使用指针去修改实际参数。这种调用称为指针调用。
指针调用相当于在调用函数时,将实际参数的内存地址传递给函数的形式参数,这样函数中就可以通过指针修改实际参数的值。
c++
#include <iostream>
using namespace std;
int func(int* a)
{
return ++*a;
}
int main()
{
int i = 1;
cout << "Value:" << func(&i) << endl;
cout << "Value:" << i << endl;
return 0;
}
输出
cmd
Value:2
Value:2
函数中修改的指针,其实就是main()函数的i变量。
结构体作函数参数
结构体可以跟普通变量一样通过参数传递给函数,传递的可以是结构体也可以是结构体的地址,当传递结构体调用时,实际上的将实际参数拷贝给形式参数,形式参数是函数的局部变量,调用结束即销毁;传递地址调用在被调用函数中访问的实际上就是实际参数本身。
c
#include <stdio.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
/* 结构体作为参数 */
int func(struct Books book1)
{
// 结构体使用(.)访问成员变量
book1.book_id += 1;
return book1.book_id;
}
/* 结构体作为指针参数 */
int func1(struct Books* book1)
{
// 指针指向的结构体,使用->访问成员变量
// 修改指针指向结构体,就是结构体本身,非函数调用时创建的局部变量
// book1实际上就是main()函数的book
book1->book_id += 1;
return book1->book_id;
}
int main() {
struct Books book;
book.book_id = 5;
int i;
/* 传值调用 */
i = func(book);
printf("func, %d, %d\n", book.book_id, i);
/* 传址调用 */
i = func1(&book);
printf("func1, %d, %d\n", book.book_id, i);
return 0;
}
输出
cmd
func, 5, 6
func1, 6, 6
func1()传递结构体的指针