0

0

0

修罗

站点介绍

只有了解事实才能获得真正的自由

指针

修罗 2024-11-06 533 1条评论 c

首页 / 正文

指针

1. 指针基础

指针是一种保存变量地址的变量;

内存其实就是一组有序字节组成的数组,数组中,每个字节大大小固定,都是 8bit。对这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。示意如下图:

image-20241105084020613.png

这是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址。

1.1 使用指针的好处

直接访问内存: 指针允许程序直接访问和操作内存中的数据。通过指针,可以避免不必要的复制操作,直接访问变量或数组的内存地址,提高程序效率。

节省内存: 使用指针传递大型数据结构(如数组或结构体)时,可以避免拷贝整个数据结构,只传递内存地址,这样可以节省内存空间,尤其是在传递大数据结构时。

实现数据结构: 指针是实现复杂数据结构(如链表、树、图等)的基础。

函数参数传递: 通过指针传递参数时,可以在函数内部修改原始变量的值。

1.2 指针表达式

*在变量左边定义指针,右边取数据:

int *p;   // 定义一个指针
int a = 10; 
p = &a; // 将a的地址赋值给指针p
int value = *p

在执行 int value = *p; 时,CPU 的操作步骤如下:

  1. CPU 读取指针 p 的值,假设 p 存储 0x100a 的地址)。
  2. CPU 通过地址总线发送 0x100 到内存,要求读取该地址的内容。
  3. 内存响应,将 0x100 地址处的值(10)通过数据总线发送回 CPU。
  4. 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;

image-20241105103739852.png

注意:要改变传入值,需通过指针传递参数;

1.3 指针定义

  1. int p; 这是一个普通的整型变量
  2. int *p; 首先从p 处开始,先与*结合,所以说明p是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以p是一个返回整型数据的指针;
  3. int p[3]; 首先从p处开始,先与[]结合,说明p是一个数组,然后与int 结合,说明数组里的元素是整型的,所以p是一个由整型数据组成的数组;
  4. int *p[3]; 首先从p 处开始,先与[]结合,因为其优先级比*高,所以p 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以p 是一个由返回整型数据的指针所组成的数组;
  5. int (*p)[3]; 首先从p处开始,先与*结合,说明p 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以p 是一个指向由整型数据组成的数组的指针 ;
  6. int **p; 首先从p 开始,先与*结合,说是p是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据;
  7. int p(int); 从p处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据;
  8. int (*p)(int); 从p处开始,先与指针结合,说明p 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以p是一个指向有一个整型参数且返回类型为整型的函数的指针 ;
  9. 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

运算符优先级

  1. ::{作用域}
  2. (){函数调用,类型构造:type(exp)},[]{下标},.{成员选择},->{成员选择}
  3. ++{后置},--{后置},typeid{类型id},explicit_cast{四种类型转换}
  4. ++{前置},--{前置},~{取反},!{逻辑非},-{一元负},+{一元正},*{指针指向值},&{取地址},(){老式类型转换},sizeof{对象大小}
  5. sizeof{类型或参数包的大小},new{分配内存},delete{释放内存},noexcept{能否抛出异常}
  6. ->*{指向成员中的指针},.*{指向成员中的指针}
  7. *,/,%
  8. +,-
  9. <<,>>
  10. <,>,<=,>=
  11. ==,!=
  12. &

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));
}

image-20241105111740750.png

注意: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);
    
}

image-20241105141901558.png

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));
}

image-20241105161307878.png

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指针分区。

评论(1)

  1. xiaolen 游客 2024-11-25 15:42 回复

    指针提供直接操作内存的方式


最新评论

  • 1

    1

  • 1

    1

  • -1' OR 2+158-158-1=0+0+0+1 or 'TKCTZnRa'='

    1

  • 1

    1

  • 1

    1

  • 1

    1

  • 1

    1

  • @@5Qa2D

    1

  • 1

    1

  • 1

    1

日历

2025年09月

 123456
78910111213
14151617181920
21222324252627
282930    

文章目录

推荐关键字: Linux webpack js 算法 MongoDB laravel JAVA jquery javase redis