目录
一、指针的基本概念
-
定义与声明
- 在 C++ 中,指针是一种变量,它存储的是另一个变量的内存地址。例如,
int *p;
定义了一个名为p
的指针变量,它可以指向一个int
类型的变量。指针变量的类型(这里是int *
)决定了它可以指向的数据类型。 - 指针可以指向各种数据类型,如
char
、float
、double
等,例如char *cPtr;
用于定义一个指向char
类型的指针,double *dPtr;
用于定义一个指向double
类型的指针。
- 在 C++ 中,指针是一种变量,它存储的是另一个变量的内存地址。例如,
-
初始化
- 指针在使用前最好进行初始化。可以将指针初始化为
nullptr
(在 C++ 11 及以上版本推荐使用),例如int *p = nullptr;
。这样可以避免出现野指针(未初始化的指针,其指向的内存位置不确定,可能导致程序崩溃或出现错误行为)。 - 也可以将指针初始化为一个已经存在的变量的地址。例如:
- 指针在使用前最好进行初始化。可以将指针初始化为
int num = 10;
int *p = #
这里&
是取地址运算符,&num
表示获取变量num
的内存地址,并将这个地址赋值给指针p
,此时p
就指向了变量num
。
-
通过指针访问变量的值
- 使用
*
(解引用运算符)可以访问指针所指向的变量的值。例如,对于上面定义的p
和num
,*p
就表示访问p
所指向的变量(即num
)的值。可以对*p
进行赋值操作来修改num
的值,如*p = 20;
,此时num
的值就变为了 20。
-
指针的大小
在 32 位系统中,指针的大小通常是 4 字节,在 64 位系统中,指针的大小通常是 8 字节。这是因为指针存储的是内存地址,32 位系统的内存地址空间是 2的32次方,用 4 字节(32 位)来表示;64 位系统的内存地址空间是 2的64次方,用 8 字节(64 位)来表示。
二、指针与数组
-
数组名作为指针
在 C++ 中,数组名可以看作是一个指向数组第一个元素的常量指针。例如,对于数组int arr[5];
,arr
本质上是一个int *
类型的常量指针,它指向arr[0]
。这意味着可以将数组名赋值给一个同类型的指针变量,如int *p = arr;
。 -
通过指针访问数组元素
可以使用指针来访问数组中的元素。假设p
指向数组arr
的第一个元素(p = arr;
),那么可以使用*(p + i)(即p[i])
的方式来访问arr[i]
。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for(int i = 0; i < 5; ++i)
{
std::cout << *(p + i) << " ";
}
这里*(p + i)
的计算方式是:p
首先指向数组的第一个元素arr[0]
,当i = 1
时,p + 1
会根据指针的算术运算规则,将指针p
向后移动一个int
类型的大小(在大多数系统中,int
类型占 4 字节),此时*(p + 1)
就访问到了arr[1]
。
-
指针与数组的边界检查
- 当使用指针访问数组元素时,需要注意不要超出数组的边界。因为 C++ 不会自动检查指针是否超出数组范围,如果超出,可能会导致程序出现未定义行为,如访问到错误的内存地址、程序崩溃等。
三、指针与函数
-
函数中的指针参数
-
传递数组
- 可以将数组作为指针参数传递给函数。当传递数组时,实际上传递的是数组的首地址。例如:
-
void printArray(int *arr, int size)
{
for(int i = 0; i < size; ++i)
{
std::cout << arr[i] << " ";
}
}
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5);
return 0;
}
在printArray
函数中,参数arr
是一个指针,它接收了在main
函数中数组arr
的首地址,这样就可以在函数内部通过指针访问数组的元素。
-
修改变量的值
- 可以通过指针在函数中修改外部变量的值。例如:
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
int num1 = 10, num2 = 20;
swap(&num1, &num2);
std::cout << "num1: " << num1 << " num2: " << num2 << std::endl;
return 0;
}
在swap
函数中,通过接收变量的地址(&num1
和&num2
),并使用解引用运算符*
来交换两个变量的值。
指向函数的指针
-
概念
- 指向函数的指针是一种特殊的指针,它存储的是函数的入口地址。函数指针的定义形式如下:
返回值类型 (*指针变量名)(参数列表);
。例如,int (*funcPtr)(int, int);
定义了一个指向返回值为int
类型,且有两个int
参数的函数的指针。
- 指向函数的指针是一种特殊的指针,它存储的是函数的入口地址。函数指针的定义形式如下:
-
初始化和使用
- 可以将函数名(函数名本身就是函数的入口地址)赋值给函数指针来初始化它。例如:
int add(int a, int b)
{
return a + b;
}
int main()
{
int (*funcPtr)(int, int) = add;
int result = funcPtr(3, 5);
std::cout << "Result: " << result << std::endl;
return 0;
}
这里funcPtr
被初始化为add
函数的地址,然后可以通过funcPtr
来调用add
函数,就像调用普通函数一样。
-
应用场景
- 函数指针可以用于实现回调函数。例如,在一个排序函数中,可以通过函数指针来传递一个比较函数,这样排序函数就可以根据不同的比较规则进行排序。假设我们有一个简单的冒泡排序函数:
void bubbleSort(int *arr, int size, bool (*compare)(int, int))
{
for(int i = 0; i < size - 1; ++i)
{
for(int j = 0; j < size - i - 1; ++j)
{
if(compare(arr[j], arr[j + 1]))
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
bool ascending(int a, int b)
{
return a > b;
}
int main()
{
int arr[5] = {3, 1, 4, 1, 5};
bubbleSort(arr, 5, ascending);
for(int i = 0; i < 5; ++i)
{
std::cout << arr[i] << " ";
}
return 0;
}
在bubbleSort
函数中,compare
是一个函数指针,它可以接收不同的比较函数(如ascending
函数),这样就可以根据不同的比较规则对数组进行排序。
四、补充
-
指针与动态内存分配
-
new
和delete
操作符- 在 C++ 中,
new
操作符用于在堆(heap)上动态分配内存。例如,int *p = new int;
会在堆上分配一个足够存储int
类型数据的内存空间,并返回这个空间的地址,将其赋值给指针p
。如果要分配一个包含多个元素的数组,可以使用int *arr = new int[5];
,这会在堆上分配能存储 5 个int
类型元素的连续内存空间。 - 与
new
相对应的是delete
操作符,用于释放new
分配的内存。对于单个对象,使用delete p;
来释放内存。对于用new[]
分配的数组,要使用delete[] arr;
来释放内存。如果不释放动态分配的内存,会导致内存泄漏,即程序占用的内存越来越多,最终可能耗尽系统资源。
- 在 C++ 中,
-
动态内存分配的优势和应用场景
- 动态内存分配的一个主要优势是可以根据程序运行时的实际需求灵活地分配内存大小。例如,在处理用户输入的数据量不确定的情况时,可以根据用户输入的元素个数动态分配数组大小。
- 它还可以用于创建复杂的数据结构,如链表、树等。以链表为例,每个链表节点通常包含一个数据成员和一个指向下一个节点的指针。可以通过动态分配内存来创建节点,如:
-
struct ListNode {
int data;
ListNode *next;
};
ListNode *head = new ListNode;
head->data = 1;
ListNode *second = new ListNode;
second->data = 2;
head->next = second;
这里动态分配了两个链表节点,并通过指针将它们连接起来。
-
指针与常量
-
常量指针
常量指针是指指针所指向的内容是常量,即不能通过这个指针来修改所指向的变量的值。定义形式为const 数据类型 *指针变量名;
。例如,const int *p;
,此时*p
不能被赋值,但是指针p
本身可以指向其他变量。 -
指针常量
指针常量是指指针本身是一个常量,不能再指向其他变量,但可以通过这个指针修改所指向变量的值。定义形式为数据类型 *const 指针变量名;
。例如,int *const p;
,一旦p
被初始化指向某个变量,就不能再让它指向其他变量,但可以对*p
进行赋值操作。 -
指向常量的指针常量
这是前两者的结合,既不能通过指针修改所指向变量的值,指针本身也不能再指向其他变量。定义形式为const 数据类型 *const 指针变量名;
。例如,const int *const p;
。
-
-
指针的类型转换
-
隐式类型转换
在某些情况下,C++ 会自动进行指针类型的隐式转换。例如,当一个void *
类型的指针(void *
可以指向任何类型的数据)被赋值给其他类型的指针时,需要进行显式类型转换(在 C 语言中可以隐式转换,但 C++ 更严格)。但是,从一个派生类指针转换为基类指针是可以隐式进行的,这在面向对象编程中的多态性中有重要应用。 -
显式类型转换(强制类型转换)
可以使用reinterpret_cast
、static_cast
、dynamic_cast
(用于类层次结构中的安全向下转型)和const_cast
等操作符来进行指针的显式类型转换。例如,reinterpret_cast
可以用于将一种指针类型转换为另一种不相关的指针类型,这种转换比较危险,可能会导致程序错误,如:
-
int *p1 = new int;
double *p2 = reinterpret_cast<double *>(p1);
这里将int
指针转换为double
指针,可能会导致对内存的错误解释,除非你非常清楚自己在做什么。
-
指针与引用的对比
-
引用的定义和使用
引用是 C++ 中的一个概念,它是一个已定义变量的别名。定义形式为数据类型 &引用变量名 = 已定义变量名;
。例如,int num = 10; int &ref = num;
,此时ref
就是num
的引用,对ref
的操作就相当于对num
的操作。 -
指针和引用的区别
指针可以不初始化,而引用必须在定义时初始化。指针可以在运行时改变所指向的对象,而引用一旦初始化,就始终引用同一个对象。另外,指针的语法相对复杂,需要使用*
和&
等操作符,而引用的使用更像是直接对原变量进行操作。例如,在函数参数传递中,使用指针可以实现传递空值(nullptr
),而引用必须绑定到一个有效的对象上。但是,引用在语法上更简洁,在某些情况下,如作为函数返回值返回对象的引用,可以避免对象的复制,提高效率。
-