指针
C 和 C ++ 原始指针
指针在 C 语言中本质就是一种数据类型,不过其他数据类型的变量是指向地址中的存储的数据,而指针是指向存储数据的地址。
指针的使用
1.指针表达式
2.二级指针
指针运算
指针类型
1.基本指针类型
基本数据类型中有 int
整型,float
浮点数等等,这些基本类型都对应着基本指针类型,例如:int*
,float*
等。如下表:
基本数据类型 | 占用空间 | 基本指针类型 | 占用空间(32 或 64 位机器) |
---|---|---|---|
char | 1 字节 | char* | 4 或 8 字节 |
short | 2 字节 | short* | 4 或 8 字节 |
int | 4 字节 | int* | 4 或 8 字节 |
long | 4 字节 | long* | 4 或 8 字节 |
long long | 8 字节 | long long* | 4 或 8 字节 |
float | 4 字节 | float* | 4 或 8 字节 |
double | 8 字节 | double* | 4 或 8 字节 |
相关信息
虽然指针的占用空间是固定的,但是在进行指针运算时,每次移动的位数与对应的基本类型相关。
基本类型指针之间进行「强制类型转换」时需要注意的问题
int a = 1025; // 转为 32 位二进制: 00000000 00000000 00000100 00000001
int* pa = &a; // pa 指向 a
/*
将 int* 强制转换为 char* , int* 本应指向对应数据的 4 个字节, 经过
强制转换之后, 只能指向第一个字节, 即:指向 00000001 所在地址, 那么转换
之后的值也就是 00000001
*/
char* pca = (char*) pa;
// 将 00000001 转为 10 进制输出
print("%d\n", *pca); // 输出结果: 1
// 进行指针运算, 将 pca 指向 00000100 所在地址并以 10 进制形式输出该地址的数据
print("%d\n", *(pca + 1)); // 输出结果: 4
void*
型指针和 NULL
以及 nullptr
2.
void*
类型指针
void*
指针类型是可以指向任意类型的数据,也即可以将 指向任意类型的指针 直接赋值 给void*
类型的指针,例如:
int* pa;
void* pv = pa; // 将 int* 赋值给 void*
若想将 void*
类型的指针赋值给其他类型的指针,则需要进行强制类型转换才可以完成赋值,例如:
int* pa;
void* pv;
pa = (int*) pv; // 将 void* 强制转换为 int*
在使用内存分配函数 malloc
函数时,由于其返回值是 void*
,则需要显式说明该指针指向的内存存放的是什么类型的数据,所以需要对其进行强制类型转换,例如:需要分配 100 个整数的内存空间:
int* pa = (int *)malloc(sizeof(int) * 100);
注意
对于 void*
型的指针,其是否可以进行指针运算取决于编译器遵循的标准,在 GNU 中,void*
在进行指针运算时就像 char*
一样;而在 ANSI 的标准,对 void*
类型的指针进行运算时不被允许的。
void*
属于无类型的指针,那么可以通过其在 C 语言中实现泛型编程。具体参考:C 语言中的泛型编程
NULL
和nullptr
NULL
本身就是一个特殊的指针变量,表示不指向任何东西,可以将 NULL
值赋值给一个指针变量使其不指向任何东西。
- 在 C 语言中
NULL
的定义如下:
#define NULL ((void *)0)
// 或者
#define NULL 0
其会根据不同类型的指针隐式类型转换为响应的类型;
int* pi = NULL; // NULL 隐式转换为 int*
char* pc = NULL; // NULL 隐式转换为 char*
- 在 C++ 中,由于C++是强类型的语言的缘故,定义如下:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
NULL
被定为为了 0
, 不会 隐式的转换为其他类型,在 C++ 11 之后往往使用 nullptr
替代使用 NULL
,nullptr
的本质是一个常量,实现代码大致如下:
const class nullptr {
public:
template<class T>
inline operator T*() const // 可向任意类型的非成员指针转换
{
return 0;
}
template<class C, class T>
inline operator T C::*() const // 可向任意类型的成员指针转换
{
return 0;
}
private:
void operator&() const; // 不可取地址
} nullptr = {};
更多细节在: nullptr
注意
- 解引用
NULL
指针是非法的行为,在解引用之前,必须保证它是非NULL
的指针。 nullptr
一些易错点
int n = 0;
void* p1 = 0;
int* p2 = nullptr;
int* p3 = 0; // OK, 0 仍然可以赋值给指针
// ----- 不能解引用赋值为 0 或 nullptr 的指针 -----
// 不会报错,但是可能导致系统崩溃
// cout << (*p2) << endl;
// cout << (*p3) << endl;
// ---------------------
cout << (p2 == 0) << endl; // OK, true
cout << (nullptr == 0) << endl; // OK, true
cout << (nullptr == p1) << endl; // OK, true
// cout << (nullptr == n) << endl; // Err
// int n = nullptr; // Err,不能将 nullptr 赋值给非指针类型
// nullptr = 0; // Err, nullptr 是 const 的,不能被赋值
// nullptr + 2; // Err, nullptr 未重载 + 号,不能使用
3.字符指针
对于 char*
指针来说,一般使用是直接作为指向某个字符的指针,例如:
char c = 'a';
char* pc = &c;
*pc = 'a';
还有一种用法是使用 char*
指向一连串字符的首个字符,使其代表这一连串的字符。
char* pstr = "hello world!";
此时 pstr
指向的字符 h
所在的地址,
char[] 和 char* 的区别
char[]
是字符数组,是数组;而 char*
是字符指针,是指针。
char sc1[] = "houdongdong";
char sc2[] = "houdongdong";
char* pstr1 = "houdongdong";
char* pstr2 = "houdongdong";
printf("%s\n", sc1); //输出: houdongdong
printf("%s\n", pstr1); //输出: houdongdong
printf("%c\n", *pstr1); //输出: h
/*
1.sc1 和 sc2 中的所有数据都是存储在栈上,并且都是单独分配的,所以其首地址是不同的.
2.pstr1 和 pstr2 指向的是同一常量字符串, c/c++ 会把常量字符串存储到单独的内存区域.
当多个指针指向相同的字符串常量时,实际上指向的同一个常量字符串。
所以 pstr1 和 pstr2 都是存在栈上的指针, 其共同指向 字符串常量区的字符串 "houdongdong"
pstr1 和 pstr2 所指向字符串常量是 不可被修改的
sc1 和 sc2 中的所有内容都是存储在栈上, 都是 可被修改的
*/
cout << (sc1 == sc2) << endl; // 输出: false
cout << (pstr1 == pstr2) << endl; // 输出: true
不同点:
char[] | char* |
---|---|
数组 | 指针 |
字符串存储在栈上,内容可修改 | 指针存储在栈上,但是其他字符串内容存储在 字符串常量区,内容不可修改 |
sizeof(var) 等于数组中所有内容占用的空间 | sizeof(var) 等于指针变量的占用的空间 |
只能使用 0 到 size - 1 之间的索引 | 其索引的本质是进行指针运算,所以任何数都可以,但是越界的访问可能会出现问题,例如:有char *pc ,而 pc[-1] 是指 指针运算 *(pc-1) |
char b[5] = {'1', '2', '3', '4', '5'};
char *ptr1 = (char *) (&b + 1);
char *ptr2 = (char *) (b + 1);
printf("%c\n", ptr1[-1]); // 输出 '5' 等价于 *(ptr1 - 1)
printf("%c\n", *ptr2); // 输出 '2'
4.数组指针
数组指针本质就是指针,例如上面的字符指针,实际就是字符数组;即指针代表数组。例如:整形指针 int* pa
可以代表数组 int pa[]
,或者 int (*pa)[10]
可以代表 int pa[][10]
。
* 和 [] 同时出现时结合的优先级顺序问题
[]
要比 *
的优先级大,所以下面表达式中:
int* pa[10]; // 指针数组
int (*pa)[10]; // 数组指针
- 上面第一个表达式中,pa 会先和
[]
结合,所以其就是数组,数组中的元素类型为int*
,即存放多个指针的数组; - 第二个表达式中,通过括号将 pa 与
*
先结合,使其成为整形指针,也即一维数组,再与[]
结合,组成二维数组,即指针 pa 代表整个二维数组。
在使用数组指针前,首先需要明白
数组名
和&数组名
的区别,通过下面代码可看出区别
int a[10] = { 0 };
printf("%p\n", a); // 000000000062fdf0
printf("%p\n", &a); // 000000000062fdf0
printf("%p\n", a + 1); // 000000000062fdf4
printf("%p\n", &a + 1); // 000000000062fe18
数组名
代表数组中 "首元素" 的地址&数组名
代表 数组 的地址
因为在 c 语言中数组的首元素代表的就是数组,所以上面两者是相同的.
a + 1
对应的地址比a
的地址大4
个字节,而数组是int
类型的,所以a + 1
代表的是数组中首元素的下一个元素的地址(也即第二个元素的地址)【一次移动一个元素】&a + 1
对应的地址比&a
的地址大40
个字节,也即是整个数组的长度,那么&a + 1
代表就是跨过整个数组中所有元素后的第一个地址。【一次移动一整个数组】
为数组指针赋值
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 一维数组
int *pb = a; // 正确, 且 此时的 pb 是一维数组
int (*pa)[10] = &a; // 正确, 且 此时的 pa 是二维数组
// int (*pa)[10] = a; // 错误, 不能将 int* 赋值给 int(*)[10]
- 因为
pa
代表的是二维数组,它可以看作多个一维数组组成的数组,那么要将一维数组赋值给二维数组时,要将该 “首元素” 的地址,也即整个一维数组的地址 (&a
) 赋值给pa
。
数组、指针作为函数参数,指针作为函数返回值
1.一维参数
void test1(int a[]) {}
void test2(int a[10]){}
void test3(int* a){}
void test4(int* a[10]){}
void test5(int** a){}
int main() {
int a[10] = {0}; // 一维基本数据类型数组
test1(a);
test2(a);
test3(a);
int *b[10] = {0}; // 一维指针数组
test4(b);
test5(b);
}
a[]
等价于a[10]
,且传参传的是数组名,是数组中首元素的地址,也即上述例子中的int*
,所以int* a
等价于int a[]
和int a[N]
;int** a
就是个二级指针,而一级指针int *a
等价于数组int a[]
,所以将二级指针中的一级指针变为数组,也即int* a[]
,这也是等价int** a
的。
2.二维参数
void test1(int a[][10]) {}
void test2(int a[10][10]){}
void test3(int (*a)[]){}
void test4(int (*a)[10]){}
int main() {
int a[10][10] = {0}; // 二维基本数据类型数组
test1(a);
test2(a);
test3(a);
test4(a);
int b[10] = {10}; // 一维基本数据类型数组
test3(&b); // test3 的参数是代表二维数组的指针,而 b 是一维数组, 所以要传递整个数组的地址
}
int a[][10]
等价于int a[10][10]
,且二维数组声明时不能省略 列值 ,具体参考:二维数组不能省略列值的原因
指针作为参数时,也和上面分析一致,只需要注意指针和数组之间的关系以及运算符结合的优先级问题即可。
3.指针作为函数返回值
5.指针数组
指针数组就是存放指针的数组,本质是数组,只是将平时使用的基本类型换成了指针。
声明形式如下:
int* pa[N]; // 一级指针数组
- 在 数组指针 中已经解释清楚优先级问题,基本的一级指针数组如上,多级指针数组也类似,理清优先级即可分清。
字符串并不是基本类型,那么要使用字符串数组时,就可以使用指针数组实现:
#include <iostream>
using namespace std;
const int MAX = 4;
int main(int argc, const char * argv[]) {
const char *names[MAX] = {
"Zara Ali",
"Hina Ali",
"Nuha Ali",
"Sara Ali",
};
for (int i = 0; i < MAX; i++) {
cout << " --- names[i] = " << names[i] << endl;
cout << " --- *names[i] = " << *names[i] << endl;
cout << endl;
cout << " --- (*names[i] + 1) = " << (*names[i] + 1) << endl;
cout << " --- (char)(*names[i] + 1) = " << (char)(*names[i] + 1) << endl;
cout << " ------------------------------------ " << endl << endl << endl << endl;
}
return 0;
}
6.函数指针
7.对象指针
8.常量指针和指针常量以及指向常量的常指针
常量指针
又称常指针,可以理解为常量的指针,是指针,且其指向的是常量,这个常量是指针的值(地址),而不是地址指向的值。也即:指向的地址是个常量。关键点:
- 常量指针指向的对象不能通过这个指针来修改,可是仍然可以通过原来的声明修改;
- 常量指针可以被赋值为变量的地址,之所以叫常量指针,是限制了通过这个指针修改变量的值;
- 指针还可以指向别处,因为指针本身只是个变量,可以指向任意地址;
例如: 标志: const *
int const* p;
const int* p;
「常量指针」多用于指针作为参数时不想通过该参数改变指向对象的值时使用。
指针常量
本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以不能被赋值。关键点:
- 它是个常量!
- 指针所保存的地址可以改变,然而指针所指向的值却不可以改变;
- 指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化;
例如: 标志:* const
int* const p;
指向常量的常指针
指向常量的指针常量就是一个常量,且它指向的对象也是一个常量。关键点:
- 一个指针常量,指向的是一个指针对象;
- 它指向的指针对象且是一个常量,即它指向的对象不能变化。
const int* const p;
实例
int a, b = 0;
//--------常量指针--------//
const int *p1 = &a;
a = 300; // OK , 仍然可以通过原来声明修改值,
// *p1 = 5; // Err, 无法通过常指针去修改其所指向的地址的 数据
p1 = &b; // OK , 常指针也可以随意指向别处
//--------指针常量--------//
int const* p2 = &a;
a = 500; // OK , 仍然可以通过原来声明修改值,
*p2 = 3; // OK , 可以通过常指针去修改其所指向的地址的 数据
// p2 = &b; // Err, 指针常量本身就是个常量,所以不能改变其值,自然也不能改变其指向
//--------指向常量的常量指针--------// 结合了上面两种类型的限制
const int * const p3 = &a;
a = 600; // OK , 仍然可以通过原来声明修改值,
// *p3 = 5; // Err
// p3 = &b; // Err
*
和 &
操作符
* 操作符 | 案例 |
---|---|
「乘号」进行乘法运算 | int a = b * c; |
「定义指针」定义指针变量时使用 | int a = 10; int* p = &a; |
「间接操作符」获取指针指向的数据 | int a = 10; int* p = &a; 则有 *p == a |
注意
当 *
作为间接操作符时,只能作用于指针类型的表达式,不能作用于常量,例如:*100 = 25
是错误的写法。
& 操作符 | 案例 |
---|---|
「逻辑与操作符」当且仅当都判断完两边条件都为假时,最后结果才是假 | 在下面 |
「按位与操作符」将按位与的操作数转化为二进制然后按位与 | 8&7 等价于二进制下1000(2) & 0111(2) ==0000(2) |
「取地址操作符」取出被使用的变量的地址 | int a = 10; int* p = &a; 则有 *p == a |
&& 与 & 的区别
相同点:
- 都可以作为逻辑与操作符,
不同点:
&&
具有短路与的特性,即如果第一个表达式为false
时,则不再计算第二个表达式,例如:
int n, m = 10, 100;
if (n != 10 && m++ == 10) { // n != 10 为 false 且 && 不会执行 m++ == 10
cout << n << " " << m << endl; // 输出结果: 10 100
}
if (n != 10 & m++ == 10) { // n != 10 为 false 且 & 会执行 m++ == 10
cout << n << " " << m << endl; // 输出结果: 10 101
}
相关信息
使用 &
按位与的功能可以实现判别整数的奇偶性:
- 当
x & 1 == 0
时为偶数; - 当
x & 1 == 1
时为奇数。
使用指针时的一些危险行为
*
操作符的使用
1.指针常量
间接操作符 *
只作用于指针类型的表达式,而不直接作用于常量。例如:
*100 = 25; // 错误
* (int *)100 = 25; // 正确
上面错误写法中,就是直接作用于常量,这是非法的,若想将数据 25 存储于地址 100
处,可以通过强制类型转换写法,即上面的正确写法。这里的地址是内存中真正存在的地址,但是由于编译器的在每次编译时,其所分配的地址都是随机的,那么这种赋值是非常危险的,一般情况下绝不会这样使用 *
操作符,除非在某种特定的、需要使用固定内存地址的情况下才会使用。
2.空指针、野指针使用时的注意点
//-------空指针-------//
int *p4 = NULL;
//printf("%d",*p4); //运行Error,使用指针时必须先判断是否空指针
//-------野指针(悬浮、迷途指针)-------//
int *p5 = new int(5);
delete p5;
p5 = NULL; //一定要有这一步
printf("%d",*p5); //隐藏bug,delete掉指针后一定要置0,不然指针指向位置不可控,运行中可导致系统挂掉
//-------指针的内存泄漏-------//
int *p6 = new int(6);
p6 = new int(7); //p6原本指向的那块内存尚未释放,结果p6又指向了别处,原来new的内存无法访问,也无法delete了,造成memory leak