指针
1. 指针基础
指针是一种保存变量地址的变量;
内存其实就是一组有序字节组成的数组,数组中,每个字节大大小固定,都是 8bit。对这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。示意如下图:
这是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址。
1.1 使用指针的好处
直接访问内存: 指针允许程序直接访问和操作内存中的数据。通过指针,可以避免不必要的复制操作,直接访问变量或数组的内存地址,提高程序效率。
节省内存: 使用指针传递大型数据结构(如数组或结构体)时,可以避免拷贝整个数据结构,只传递内存地址,这样可以节省内存空间,尤其是在传递大数据结构时。
实现数据结构: 指针是实现复杂数据结构(如链表、树、图等)的基础。
函数参数传递: 通过指针传递参数时,可以在函数内部修改原始变量的值。
1.2 指针表达式
*
在变量左边定义指针,右边取数据:
int *p; // 定义一个指针
int a = 10;
p = &a; // 将a的地址赋值给指针p
int value = *p
在执行 int value = *p;
时,CPU 的操作步骤如下:
- CPU 读取指针
p
的值,假设p
存储0x100
(a
的地址)。 - CPU 通过地址总线发送
0x100
到内存,要求读取该地址的内容。 - 内存响应,将
0x100
地址处的值(10
)通过数据总线发送回 CPU。 - CPU 接收到数据,将
10
存储到寄存器或变量value
中。
1.2.1 案例:变量值交换
改变指针指向的值:
int swap(int *x, int *y) {
printf("swap before x: %d y:%d\n", *x, *y);
int tmp = *x;
*x = *y;
*y = tmp;
printf("swap after x: %d y:%d\n",*x, *y);
}
int a = 10; int b = 20;
swap(&a, &b);
std::cout << "a: " << a << " b: "<<b << std::endl;
注意:要改变传入值,需通过指针传递参数;
1.3 指针定义
int p;
这是一个普通的整型变量int *p;
首先从p 处开始,先与*
结合,所以说明p是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以p是一个返回整型数据的指针;int p[3];
首先从p处开始,先与[]结合,说明p是一个数组,然后与int 结合,说明数组里的元素是整型的,所以p是一个由整型数据组成的数组;int *p[3];
首先从p 处开始,先与[]结合,因为其优先级比*
高,所以p 是一个数组,然后再与*
结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以p 是一个由返回整型数据的指针所组成的数组;int (*p)[3];
首先从p处开始,先与*
结合,说明p 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以p 是一个指向由整型数据组成的数组的指针 ;int **p;
首先从p 开始,先与*
结合,说是p是一个指针,然后再与*
结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据;int p(int);
从p处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据;int (*p)(int);
从p处开始,先与指针结合,说明p 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以p是一个指向有一个整型参数且返回类型为整型的函数的指针 ;int *(*p(int))[3];
p(int)
表示函数p
接受一个int
类型的参数。(*p(int))
表示p
的返回值是一个指针。[3]
表示返回的是一个指向有 3 个元素的数组的指针。int *
表示数组的元素类型是int *
。
因此,返回值是指向包含 3 个 int *
的数组的指针。
1.4 指针类型
1.4.1 指针自身类型:
把指针名字去掉,剩下的就是指针类型:
int * ptr; // 指针的类型是int *
char * ptr; // 指针的类型是char *
int ** ptr; // 指针的类型是int **
int( * ptr)[3]; // 指针的类型是int(*)[3]
int * ( * ptr)[4]; // 指针的类型是int*(*)[4]
1.4.2 指针所指向的类型:
int * ptr; // 指针所指向的类型是int
char * ptr; // 指针所指向的的类型是char
int * * ptr; // 指针所指向的的类型是int*
int( * ptr )[3]; // 指针所指向的的类型是int()[3], 步长12;int()[3]新类型,占用长度3 * 4
int *ptr[3]; // 指针所指向的的类型是int[3], 步长8
int * ( * ptr)[4]; // 指针所指向的的类型是int *()[4]
每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?
注意:指针指向的值和赋值的数据为不同类型,是错误的:
int *q;
*q = 100l; // long类型赋值给指向int的指针,错误
指针类型不同,赋值可能会丢失数据:
int a = 0xaabbccdd;
int *p1 = &a;
char *p2 = &a;
printf("%x\n", *p1); // aabbccdd
printf("%x\n", *p2); // ffffffdd
&a
指针指向类型是int的,占4个字节,将指针指向类型为int的指针赋值给指向类型为char的,取值时会只取一个字节;注:打印的*p2
只看dd即可,忽略前面的f
运算符优先级
::
{作用域}()
{函数调用,类型构造:type(exp)},[]
{下标},.
{成员选择},->
{成员选择}++
{后置},--
{后置},typeid
{类型id},explicit_cast
{四种类型转换}++
{前置},--
{前置},~
{取反},!
{逻辑非},-
{一元负},+
{一元正},*
{指针指向值},&
{取地址},()
{老式类型转换},sizeof
{对象大小}sizeof
{类型或参数包的大小},new
{分配内存},delete
{释放内存},noexcept
{能否抛出异常}->*
{指向成员中的指针},.*
{指向成员中的指针}*,/,%
+,-
<<,>>
<,>,<=,>=
==,!=
&
1.5 指针步长
指针指向类型决定步长长度;
1.5.1 指针指向类型为非指针类型
int a = 0xaabbccdd;
int *p1 = &a;
char *p2 = &a;
printf("%x\n", p1); // 43fd80
printf("%x\n", p1 + 1); // 43fd84
printf("%x\n", p2 + 1); // 43fd81
指针指向类型为int步长为4,char为1。
1.5.2 指针指向类型为指针类型
如下,变量c的指针指向类型为int *
,是一个指针:
int a = 10;
int *b = &a;
int **c = &b;
printf("%x\n", c); // e096f958
printf("%x", c+1); // e096f950
指针大小固定8个字节,32位编译器4个字节。
1.5.3 指针指向类型为新类型
int( * ptr )[3];
指针所指向的的类型是int()[3]
, 步长12;int()[3]
为新类型,占用长度3 * 4
;
2 数组
数组是一个比较特殊的指针,可以理解为:数组=指针,指针=数组;
int arr[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
数组arr指向第一个元素,即*arr
为1,*(arr+1)
为2;
获取数组元素:
int *p = arr; // 指针=数组
for (int i = 0; i < 8; i++) {
printf("%d ", *(p + i));
printf("%d ", arr[i]);
printf("%d ", p[i]);
printf("%d \n", *(arr + i));
}
注意:arr只能够读不能写,比如:arr = p
。
指针和数组是完全等价的吗?
数组在以下两种场景下不能作为指针常量:
- 当数组名作为sizeof操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。
int arr[10];
printf("sizeof(arr):%d\n", sizeof(arr)); //此时sizeof结果为整个数组的长度
- 当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。
int b[3] = {10, 20, 30};
printf("*b=%d\n ", sizeof(*b)); // 4
printf("b=%d\n ", sizeof(b)); // 12
printf("&b=%d\n ", sizeof(&b)); // 8
数组的传参形式
void test01(int arr[]) void test01(int arr[1]) void test01(int *) int arr[1] = { 100 }; test01(arr);
2.1 指针数组
指针数组是每个元素都是指针的数组。
int a = 10;
int b = 20;
int c = 30;
int *p1 = &a;
int *p2 = &b;
int *p3 = &c;
int * arr[3] = { p1, p2, p3 }; // 整型指针数组, 可以把* arr看成二级指针
printf("%d \n", *arr[0]); // 10
printf("%d \n", **arr); // 10
指针类型转换
char *aa = arr;
printf("%d \n", **((int **)aa)); // 10
2.1.1 堆区指针数组
char** allocate_memory(int n){
if (n < 0 ){
return NULL;
}
char** temp = (char**)malloc(sizeof(char*) * n);
if (temp == NULL){
return NULL;
}
// 分别给每一个指针malloc分配内存
for (int i = 0; i < n; i ++){
temp[i] = static_cast<char *>(malloc(sizeof(char) * 30));
sprintf(temp[i], "%2d_hello world!", i + 1);
}
return temp;
}
// 打印数组
void array_print(char** arr,int len){
for (int i = 0; i < len;i++){
printf("%s\n",arr[i]);
}
printf("----------------------\n");
}
// 释放内存
void free_memory(char** buf,int len){
if (buf == NULL){
return;
}
for (int i = 0; i < len; i ++){
free(buf[i]);
buf[i] = NULL;
}
free(buf);
}
int main() {
int n = 10;
char** p = allocate_memory(n);
array_print(p, n);
free_memory(p, n);
}
2.2 数组指针
数组指针是指针,指向数组的指针。
定义方式一:
typedef int(ArrayType)[10]; // 定义数组类型ArrayType
// ArrayType myarr; // 等价于 int arr[10];
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
ArrayType* pArr = &arr; // 定义了一个数组指针pArr,并且指针指向数组arr
方式二:
int arr[10];
// 定义数组指针类型
typedef int(*ArrayType)[10];
ArrayType pArr = &arr; // 定义了一个数组指针pArr,并且指针指向数组arr
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
方式三:
int arr[10];
int(*pArr)[10] = &arr;
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
2.3 二维数组
定义一个3行4列的二维数组:
int a[3][5] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
// 或者:int a[3][6] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
从概念上理解,a 的分布像一个矩阵:
0 1 2 3
4 5 6 7
8 9 10 11
但在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存
0 1 2 3 4 5 6 7 8 9 10 11
二维数组: &a
(指向整个数组)和a
(指向一行)和*a
地址一样
遍历:
for(int i=0; i<3; i++){
for(int j=0; j<4; j++)
printf("value :%d\n",*(*(a+i)+j));
}
2.3.1 二维数组的3种形式参数
void PrintArray01(int arr[3][7])
void PrintArray02(int arr[][8])
void PrintArray03(int(*arr)[3])
3. 函数指针
程序中定义一个函数,在编译时系统会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。用一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针;void func(int a,int b)
中func、 &func、 *func
地址一样;
int(*p)(int, int);
这个语句就定义了一个指向函数的指针变量 p, 该指针变量可以指向返回值类型为 int,且有两个整型参数的函数。首先它是一个指针变量,所以要有一个*
,即( * p
);
所以函数指针的定义方式为:函数返回值类型 (*
指针变量名) (函数参数列表);
需要注意的是,指向函数的指针变量没有 ++ 和 -- 运算。
3.1 如何用函数指针调用函数
int Func(int x); /*声明一个函数*/
int (*p) (int x); /*定义一个函数指针*/
p = Func; /*将Func函数的首地址赋给指针变量p*/
// 调用:
p(10);
(*p)(10);
3.2 函数指针作为回调函数
int test(int a,int b,int (*callback)(int)){
int reslut = a + b;
callback(reslut);
}
int call(int result){
printf("------->%d", result);
}
test(30, 40, call);
3.3 函数指针数组
- 数组
int Array[N];
- 指针的数组
int *Array[N];
- 函数的指针的数组
int (*function_pointer)[N](int,int);
按照道理应该写成 int (*func)(int a)[2];
但是要写成int (*func[2])(int a);
int (*func[2]) (int a);
先读标识符,和他相邻的是指针符号,那么func是一个指针,指向的是一个函数,
然后往右,是个数组,那么func应该指向一个数组,这个数组里存的是参数是一个int,返回值是一个int类型的函数.
意思是这两个格子里存的是两个函数的地址.
例:
int jia(int a, int b) {
return a + b;
}
int jian(int a, int b) {
return a- b;
}
int cheng(int a, int b) {
return a *b;
}
int chu(int a, int b) {
return a / b;
}
int (*pn[4])(int a, int b) ={jia, jian, cheng, chu };//定义一个函数指针
int a = 30;
int b = 20;
for (int i = 0; i < 4; i++) {
printf("result:%d\n", pn[i](a, b));
}
3.4 改变函数指针变量
int jia(int a, int b) {
return a + b;
}
int jian(int a, int b) {
return a- b;
}
void change(int (**p1)(int a,int b)){
*p1 = jia;
}
int (*pn)(int a, int b) = jia; // 一开始为加法
printf("reslut: %d\n ", pn(20,10)); // 30
pn = jian; // 修改为减法
printf("reslut: %d\n ", pn(20,10)); // 10
change(&pn); // 重新改为加法
printf("reslut: %d\n ", pn(20,10)); // 30
4. 常量指针和指针常量
常量指针,指针指向的内存空间不能修改,但可修改指针的指向
int a = 10;
int b = 20;
// const放在*号左侧
const int* p_a = &a;
// 或者 int const *p_a;
// *p_a = 100; // 不可修改指针指向的内存空间
p_a = &b; // 可修改指针的指向
指针常量,指针的指向不能修改,但是可修改指针指向的内存空间
// const放在*号的右侧
int* const p_b = &a;
// p_b = &b; // 不可修改指针的指向
*p_b = 100; // 可修改指针指向的内存空间
指针的指向和指针指向的内存空间都不能修改
const int* const p_c = &a;
5. 字符串指针
字符串是以0或者\0
0
NULL
结尾的字符数组(字符串本身一个一维数组),(数字0和字符\0
等价)
- 字符串定义方式一:
char str[6] = {'h','e','l','l','o','\0' };
printf("%s\n",str); // hello
- 方式二:
char str2[] = "hello";
printf("%s\n",str2); // hello
这种方式hello后面会自动添加\0
- 方式三:
char *str3 = "hello";
printf("%s\n",str3); // hello
这种方式字符串在常量区(上面两种在栈区),可以修改指针指向,但是不能修改字符串内容;类似指针常量;
5.1 字符串长度
char str4[] = "hello";
printf("sizeof str:%d\n", sizeof(str4)); // 6
printf("strlen str:%d\n", strlen(str4)); // 5
char str5[100] = "hello";
printf("sizeof str:%d\n", sizeof(str5)); // 100
printf("strlen str:%d\n", strlen(str5)); // 5
- sizeof 计算数组大小,数组包含
\0
字符 - strlen 计算字符串的长度,到
\0
结束
5.2 字符串合并
void mystrcat(char *s1,char * s2){
while(*s1)s1++;
while(*s1++=*s2++);
}
char s1[] = "abc";
char s2[] = "123";
mystrcat(s1, s2);
printf("%s\n",s1); // abc123
5.3 字符串数组
- 方式一:
char arr[4][10] = {"abc", "efg", "hij", "klm"};
// arr[0] = "frg"; 不能重新赋值
strcpy(arr[0], "aaaaa"); // 可以修改内容
printf("value %s\n", arr[0]); // aaaaa
不能重新赋值,但是可以修改内容, 说明是指针常量;
- 方式二:
char *arr1[4]={"abc", "efg", "hij", "klm"};
arr1[0] = "ABC"; // 能重新赋值
printf("value %s\n", arr1[0]); // ABC
// strcpy(arr1[0], "BBBBB"); // 不可以修改内容
不可以修改内容,但是能重新赋值, 说明是常量指针;
6. 异常指针
空悬指针:指针正常初始化,曾指向过一个正常的对象,但是对象销毁了,该指针未置空,就成了悬空指针。
野指针 :未初始化的指针,其指针内容为一个垃圾数。 (一般我们定义一个指针时会初始化为NULL或者直接指向所要指向的变量地址,但是如果我们没有指向NULL或者变量地址就对指针进行使用,则指针指向的内存地址是随机的)。存在野指针是一个严重的错误。
int *p; // 指针未初始化,此时 p 为野指针
int *pi = nullptr;
{
int i = 6;
pi = &i; // 此时 pi 指向一个正常的地址
*pi = 8; // ok
}
*pi = 6; // 由于 pi 指向的变量 i 已经销毁,此时 pi 即成了悬空指针
// 注意:虽然pi指向的i被销毁了,但是i的指针还存在, 代码块不像函数执行到了会进栈出栈,自动释放指针,若是函数,则会释放指针
6.1 空指针与NULL指针
什么是空指针?
如果 p 是一个指针变量,则 p = 0; p = 0L; p = '\0'; p = 3 - 3; p = 0 * 17;
中的任何一种赋值操作之后(还可以是 p = (void*)0;
), p 都成为一个空指针,由系统保证空指针不指向任何实际的对象或者函数。反过来说,任何对象或者函数的地址都不可能是空指针。
什么是NULL指针?
NULL 是一个标准规定的宏定义(定义的值就是0),用来表示空指针常量。因此,除了上面的各种赋值方式之外,还可以用 p = NULL; 来使 p 成为一个空指针。NULL指针只不过是空指针的一种例子。
为什么通过空指针读写的时候就会出现异常?
NULL指针分配的分区:其范围是从 0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。
xiaolen 游客 2024-11-25 15:42 回复
指针提供直接操作内存的方式