0

0

0

修罗

站点介绍

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

指针

修罗 2024-11-06 765 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 回复

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


最新评论

  • https://www.studyglobus.com/author/carroll34a5552/

    Ценники на вывоз мусора сейчас космос, любые лайфхаки по экономии в тему.

  • Richardgrold

    1xbet onlayn kazino platforması ilə vaxtınızı daha səmərəli və maraqlı keçirin. Saytın canlı kazino bölməsi istifadəçilərə fərqli bir dünya təklif edir. Burada canlı dilerli kazino oyunlarının hər bir növünü sınaqdan keçirə bilərsiniz. Krupyelərin peşəkarlığı oyunun dinamikasını və marağını artırır. Müntəzəm olaraq yenilənən oyun siyahısı cansıxıcılığa yol vermir. Hesabınızı artırmaq üçün onlarla fərqli və etibarlı üsul mövcuddur. Qazanılan pullar cəmi bir neçə dəqiqə ərzində şəxsi hesabınıza köçürülür. Aktiv fəaliyyətinizə görə fərdi bonuslar və hədiyyələr qazana bilərsiniz. Dəstək xidməti həm çat, həm də e-poçt vasitəsilə kömək göstərir. 1xbet ailəsinə qoşulun və böyük uduşlar üçün ilk addımı atın. onlayn kazino

  • 砂岩白丘

    我一直梦想, 看到你们相册那样的地方。真棒。

  • 城市地標

    我喜欢, 这里分享真实经验。你的项目 就是 属于这里的。请继续。

  • 宗教地標

    我热爱这样的想法, 去那么多国家。真的很鼓舞。

  • 觀景高地

    能感受到热爱。继续保持 感受。

  • 最佳季節

    我关注这样的资源, 写得很实在。你的网站 就是 属于这里的。加油。

  • 1

    1

  • 1

    1

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

    1

日历

2026年07月

   1234
567891011
12131415161718
19202122232425
262728293031 

文章目录

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