一、 指针#

1. 什么是指针#

指针其实就是一个变量,不过它的值是一个内存地址 , 这个地址可以是变量或者一个函数的地址

当你声明明一个变量的时候,计算机会将指定的一块内存空间和变量名进行绑定;这个定义很简单,但其实很抽象,例如:int x = 5; 这是一句最简单的变量赋值语句了, 我们常说“x等于5”,其实这种说法是错误的,x仅仅是变量的一个名字而已,它本身不等于任何值的。这条statement的正确翻译应该是:“将5赋值于名字叫做x的内存空间”,其本质是将值5赋值到一块内存空间,而这个内存空间名叫做x。切记:x只是简单的一个别名而已,x不等于任何值。

img

  • 为什么需要指针?

不就是使用变量或者调用函数吗?难道不能直接调用吗?那么需要指针做什么呢?

1
2
3
4
5
实际上是可以的,但是并不是所有的情况都可以。比如:
1. 在内部函数中,可以使用指针访问外部函数中定义的某个变量x, 因为它并不是声明在自己的函数范围内。
2. 指针在处理函数传递数组的时候非常高效
3. 我们还可以在堆中申请一块动态内存,这块内存甚至没有一个变量名称,唯一的访问方式是通过指针。
4. 可以使用你指针访问指定的内存地址(游戏修改器)

2. 指针使用#

1. 声明指针#

1.声明指针的时候要记得初始化,如果没有初始化,指针存放的将会是垃圾数据(因为你根本不知道它指向何方)

  1. 可以使用nullptr(c++11)进行指针初始化,初始化存放的值是 0
1
2
3
4
5
6
变量类型  *指针名称;

int * int_ptr;
double * double_ptr;
char * char_ptr;
string * strng_ptr;

2. 初始化指针#

指针的指向是一个块内存地址,如果仅仅是声明而未初始化,那么指针的指向无法得到保证,如果没有明确、指针的指向,那么最好把它初始化成一个空指针,也就是表示目前没有指向,空指针的值是0

1
2
3
4
5
6
7
8
//初始化指针
int a = 3 ;
int *p0 = &a; //p0是一个指针,指向的是一个int类型的数据,这个数据是3.

//空指针
int *p1 = nullptr;
int *p2 = NULL;
int *p3 = 0 ;

3. 指针地址和大小#

指针实际上也是一个变量,也会有自己的内存空间,也会有自己的长度大小。获取指针的内存地址,依然使用取地址符 & , 长度大小依然使用sizeof 来获取

1
2
3
4
5
6
7
int age = 88;
int *p = &age;

cout << "指针的地址是: " << &p <<endl;
cout << "指针存储的是: " << p <<endl;
cout << "指针大小是: " << sizeof p <<endl;
cout << "age的大小是: " << sizeof age <<endl;

3. 指针dereference( 解引用)#

所谓的指针dereference就是,指针就是一个变量,存放的是一个地址。这个地址有可能是变量 a 或者是变量b的地址。有了这个地址,我们可以通过dereference操作符 * 去获取到a对应的值或者b对应的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
using namespace std;

int main(){

    //定义一个变量score,赋值100
    int score {100}

    //定义一个指针score_ptr 指向score的地址。
    int *score_ptr{&score};

    //通过指针,获取到指向位置的数据 打印100
    cout << *score_ptr << endl; 

    //使用指针修改原来的score
    *score_ptr = 200 ;

    //使用指针和变量的方式打印score,结果都输出200
    cout << *score_ptr << endl;  
    cout << score << endl; 
    return 0 ;    
}

4. 动态内存分配#

在进行编码的时候,我们根本不知道需要多少内存空间。举个例子,比如我们需要存储学生的数据,这时候可以使用数组来存储,那么就必须知道学生的具体人数。如果不知道,就无法使用数组了。实际上之前学过的vector就是使用动态内存。但是有时候,我们如果需要存放的是一单个对象数据,并不是一堆数据。用vector就有点浪费了。

为了解决上述问题,C++ 提供了一种“动态内存分配”机制,使得程序可以在运行期间,根据实际需要,要求操作系统临时分配一片内存空间用于存放数据。此种内存分配是在程序运行中进行的,而不是在编译时就确定的,因此称为“动态内存分配”。申请动态内存

1. 申请内存#

可以使用new 关键字来申请动态内存 , new 开辟出来的空间都位于堆内存中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
using namespace std;

int main(){
    //定义一个int类型的指针,并没有指向任何地方。
    int * int_ptr{nullputr};

    //在堆中申请内存,使用指针指向
    int_ptr = new int ; 

    //由于未赋值,所以输出的可能是未知的值
    cout << *int_ptr << endl; 

    //修改开辟空间的数据值为100
    *int_ptr = 100;

    //解引用,输出100
    cout << *int_ptr << endl;
    return 0 ;
}

2. 释放内存#

new 常和 delete 成对出现,使用 new 开辟空间, 使用 delete 释放申请的内存,避免造成内存泄漏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include<iostream>
using namespace std;

int main(){

    int *int_ptr{nullptr};

    int_ptr = new int ; //申请内存
    ...
    delete int_ptr ;     //释放内存

    return 0 ;   
}

3. 数组操作#

使用new int[] 来给数组申请动态内存 , 然后使用 delete[] 释放申请的内存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;

int main(){

    int *array_ptr{nullptr};
    int size{};
    cout << “你期望的数组大小是:” << endl;
    cin >> size ;

    array_ptr = new int[size];

     //释放申请的空间
    delete [] arrray_ptr;

    return 0 ;   
}

4. 关于动态内存的思考#

通常情况下,定义的变量存储的位置位于栈内存中,栈内存的数据,当函数执行结束后即会被释放,这是栈内存的机制自己决定的,并且栈内存中由于内存并不是太大,所以不建议大量的数据存放在栈内存。

而堆内存中的容量相比栈内存要大多了,但是堆内存并不提供回收释放的工作,允许程序申请内存空间,但是同时也要自己负责内存空间的释放工作。

不能一概而论哪一种是最优的决定,要根据开发场景来决定。

5. 数组和指针的关系#

数组其实和指针是存在一些内在联系的,如下:

  1. 根据数组名字取到的内存地址,是数组的第一个元素地址
  2. 指针其实是一个变量,这个变量存放的值是内存地址
  3. 如果一个指针和数组是同样的类型,并且指针存放的地址正好是数组的某个元素地址,那么可以通过该指针操作数组

1. 数组与指针#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
using namespace std;

int main(){

    //定义3个长度的int类型数组
    int scores []{100, 95 , 98};

    //直接打印数组,实际上是打印数组第一个元素的地址   0x61fec8
    cout << scores  << endl;  

    //使用*操作符是根据地址获取数据,所以取到的是第一个元素 : 100
    cout << *scores  << endl; 

     //声明指针,存放的是数组第一个元素的地址
    int *score_ptr{scores};  

    //打印指针,其实输出它保存的地址,即数组首元素地址 0x61fec8
    cout << score_ptr  << endl;  

    //解引用,输出的是数组的首元素 100
    cout << *score_ptr  << endl; 

    return 0 ;
}

2. 指针运算#

如果指针指向的是数组的第一个元素地址,那么同样可以通过对指针进行加减运算,来获取其他的元素 。值得注意的是,指针的相加并不是单纯数字上的相加,而是指针对应类型占用字节的相加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<iostream>
using namespace std;

int main(){

    //定义3个长度的int类型数组
    int scores []{100, 95 , 98};

    //定义一个int类型指针,指向的是数组的首元素
    int *score_ptr{scores};

    //使用数组的手法打印数组
    cout << score_ptr[0]  << endl; //100
    cout << score_ptr[1] << endl; //95
    cout << score_ptr[2] << endl; //98


    //对指针进行加法运算。由于score_ptr 是int类型,
    //而int类型占用4个字节,所以每次相加打印出来的地址都会变长4个字节
    cout <<score_ptr  << endl; // 0x7fffacde9420
    cout <<(score_ptr+1)  << endl; // 0x7fffacde9424
    cout <<(score_ptr +2) << endl; // 0x7fffacde9428

    //指针解引用取值
    cout <<*score_ptr  << endl; // 100
    cout <<*(score_ptr+1)  << endl; // 95
    cout <<*(score_ptr +2) << endl; // 98

    return 0 ;
}

6. 指针算数#

指针除了表示存储的是内存地址之外,它也可以做算术运算 和 比较大小。

指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。

1. 指针递增#

指针递增 , 如果是数组操作,可以指向后一个元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int score[]{90,85,100};
int * score_ptr{score}

for(int i{0} ; i<3 ; i++){

    cout << ptr << endl;
    cout << *ptr << endl;

    ptr++; // 指针移动指向下一个元素
}

2. 指针递减#

指针递减 , 如果是数组操作,可以指向前一个元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int score[]{90,85,100};
int * score_ptr;

score_ptr = score[2]; //指针指向的是数组最后一个元素内存地址

for(int i{3} ; i>0 ; i--){

    cout << ptr << endl;
    cout << *ptr << endl;

    ptr--; // 指针移动指向下一个元素
}

3. 等价判断#

两个指针的等价判断,实际上是他们指向的地址比较

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
using namespace std;

int main(){
    string s1{"张三"};
    string s2{"张三"};

    string *p1 {&s1};
    string *p2 {&s2};
    string *p3 {&s1};


    cout << (p1 == p2) <<endl;  //false 
    cout << (p1 == p3) <<endl;  //true

    cout << (*p1 == *p2) <<endl;  //true 
    cout << (*p1 == *p3) <<endl;  //true

    return 0 ;

}

7. 指针与常量#

1. 指针常量#

const int *p 表示指针指向常量 , 不允许修改对应的值,但是可以指向别的地方, 这和常量修改值一样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int main(){

    //定义高分和低分的变量
    int high_score{100};
    int low_score{75};

    //使用指针常量指向高分。这里的const修饰的是指向的数据
    const int *score_ptr {&high_socre};

    //不允许修改值,因为此时编译器会认为high_score 是一个常量
    *score_ptr = 86 ; // 错误

    //可以指向其他位置。
    score_ptr = &low_score ; // 正确

    //当然也可以通过变量方式来修改值。
    high_score = 88;

    return 0 ;
}

2. 常量指针#

int* const p 表示这个指针是常量指针,不能再指向别的地方了,但是可以修改目前指向地方的值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int main(){

    int high_score{100};
    int low_score{75};

    //表示这个指针是一个常量,这里的const修饰的是指针
    int *const score_ptr{&high_score};

    //允许修改值,但是不允许再做其他指向
    *score_ptr = 86 ; // 正确
    score_ptr = &low_score ; // 错误
    return 0 ;
}

3. 常量指针指向常量#

const int* const p 表示这个指针是常量指针,并且它指向的位置的值也是常量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int main(){

    int high_score{100};
    int low_score{75}l

    //第一个const是修饰指向的数据,第二个const是修饰指针。
    //表示不管是指针还是指向的数据,都是常量 
    const int *const score_ptr{&high_score};

    //既不允许修改指向,也不允许修改指向的值
    *score_ptr = 86 ; // 错误
    score_ptr = &low_score ; // 错误 

    return 0 ;
}