C++ 入狱日记 002 —— C++ 指针详解

距离上次学习 C++ 有半年了,说起来也悲哀,上次不知道什么原因竟然半途而废了(T_T)这次因为要学习 Qt 的原因重新把 C++ 捡起来了,既然来了,就走不了了。这里我会把在学习 C++ 时碰到的一些难理解的东西用自己的理解在 csdn 整成博客,方便自己以后再次半途而废想捡起来的时候使用……

(对了,我好像记起来了,上次就是一直没理解指针才半途而废的……)

目录

1. 指针是什么

1.1 指针的意义

1.2 指针的类型

2. C++ 指针基本操作

2.1 指针的初始化和取地址、解引用

2.2 指针左值解引用

2.3 指针的算数操作

3. C++ 指针进阶

3.1 指针传参

3.2 const 指针

3.3 指针的数组和数组的指针

3.4 指针的指针

4. 小结


1. 指针是什么

1.1 指针的意义

C 语言之所以被广泛称赞运行速度快,贴近底层,有一大半原因是因为指针。C++ 完美的继承了这个特点。指针是 C++ 中的一个核心概念,也是一把双刃剑,好的一方面是它贴近底层,可以实现一些非常优化的程序;而坏的一方面也是因为它贴近底层,导致很多安全问题,而且在大型项目中错误难以调试…… 所以后来 Java Python 等高级语言一起弃用了指针,指针在实际开发中也就没那么重要了。但是我们既然来学习 C++ 了,就是奔着编写一个运行速度快的程序的。所以指针这个概念还是必须要知道的~

指针(Pointer),说白了就是内存地址。计算机的内存可以看作一条街,每一栋房子里存储的是一个数值。不知道大家有没有见过那种贴在店铺外的蓝色小牌子,上面写着 XX街XX号。那个牌子是店铺在一整条街的地址。同样的,指针也是一个数值在内存中的地址。所以,指针你可以把它理解为数值上贴的地址小牌子~

蓝色的牌子就是指针(绘图工具:Gitmind)

 此外有些人有一个疑惑:有变量名,为什么还要有指针?其实这个问题很好解答。可以想想,比如上图的“人挺多养老院”就相当于变量名,“美食街 9 号”就相当于地址。如果一个陌生人知道有一个“人挺多养老院”,但不知道它的地址“美食街 9 号”,那他就没法找到它了。这个道理也适用于内存。

指针在底层是使用 16 进制表示的,比如 0039F894。看到这个不要慌,它其实就是内存门牌号(地址)的表示方式。

1.2 指针的类型

指针也有类型,一种类型的指针只能指向一种类型的变量。内存最底层其实就是 0 和 1,一个 0 或者一个 1 所占用的内存空间就叫做位(bit),8 个位就是一个字节(byte)。每一种数据类型,都有自己在内存底层占用的字节数。比如 int 占用 4 个字节,short 占用 2 个字节。如果指针它只代表 2 字节的内存,那它所指向的那段内存就只有 int 的前两个字节,也就不能有效地解析 int 值了。所以指针的类型确保了有效解析指针所指的值,它规定了指针所指地址字节的长度。

指针的类型与它解析的值(绘图工具:Gitmind)

2. C++ 指针基本操作

2.1 指针的初始化和取地址、解引用

指针的初始化非常简单,就是类型名 *指针名。其中指针名前面要加一个星号以表明这是一个指针而不是普通变量。在初始化多个指针时,星号要加在每一个指针名的前面。

#include <iostream>
using namespace std;

int main(){
    // 创建一个指针
    // 指针的创建方式:类型名 *指针名
    int *pointer;

    // 多个指针同时创建每个指针名前面都要加 * 号
    int *ptr1, *ptr2, *ptr3;
    return 0;
}

然后有两个关于指针的运算符我们要认识一下:取地址和解引用。取地址是在变量前加一个 & 号,取完的地址可以直接赋值给指针。而解引用则是在指针前加一个 * 号(请注意这个 * 号与上面初始化指针的那个 * 号意义不同),解出来的值就相当于原变量的值。有了这两个运算符我们就可以对指针进行实际的应用。

(请注意初始化指针时的 * 号仅在初始化时使用,指针也属于变量的一种,实际使用指针时不需要加 * 号。如果加上 * 号了就代表你把它解引用了)

#include <iostream>
using namespace std;

int main(){
    // 创建一个 int 变量 var
    int var = 3;

    // 对 var 进行取地址并赋值给指针,请注意指针的类型也要是 int
    int *varPtr = &var;
    
    // 打印 varPtr 指针指向的内存地址
    // 请注意初始化指针时的 * 号仅在初始化时使用,指针也属于变量的一种,实际使用指针时不需要加 * 号
    cout << "varPtr:" << varPtr << endl;

    // 对 varPtr 进行解引用并打印
    cout << "*varPtr:" << *varPtr << endl;

    // 打印原变量 var 的值并验证是否相符
    cout << "var:" << var << endl;

    return 0;
}

运行结果(varPtr 的值每次运行都不同,这取决于操作系统的分配,在此不加赘述):

一般的指针如果没有想好指向什么变量要先赋值为 NULL(内存地址中地址 0 的地方),要不然到时候如果不小心对这个指针解引用的话,等待你的可能是一串是人是鬼都看不懂的乱码。定义为 NULL 也方便判断这个指针是否指向有效值是否可以解引用。

#include <iostream>
using namespace std;

int main(){
    // 创建一个空指针并赋值为 NULL
    int *pointer = NULL;

    // 判断指针是否为空,如果不为空就将其解引用并打印
    // 当指针为空时,对其强制解引用会报错
    if ( pointer != NULL ){ // 也可写成 if ( pointer ),这个方式运用了 int 与布尔值的隐式转换
        cout << "*pointer:" << *pointer << endl; 
    } else {
        cout << "Pointer is NULL." << endl;
    }

    return 0;
}

运行结果:

2.2 指针左值解引用

所谓“左值解引用”,就是把指针的解引用值当作赋值语句中的左值来赋值,比如 *指针 = 值。指针左值解引用可以直接改变指针所指原变量的值。指针的解引用其实挺有趣的:当你想把它当作一个 int 值来使用时,它是一个普通的 int 值;而当你把它当作左值时,它又像 var num 这些变量名一样可以赋值使用,并且影响到原变量的值。

#include <iostream>
using namespace std;

int main(){
    int num = 4;

    int *numPtr = &num;

    cout << "num = " << num << endl;

    // 指针左值解引用
    *numPtr = 3;

    cout << "num = " << num << endl;

    return 0;
}

运行结果显而易见:

2.3 指针的算数操作

指针也可以像普通数字那样进行算数操作,但是只有加减没有乘除。还有指针加(减)上几,实际上是指加(减)上了几个类型大小,如 int 指针加上 1 实际上是加上了一个 int 的大小 4 字节。

​​​#include <iostream>
using namespace std;

int main(){
    // 新建一个数组
    int arr[5] = {1,2,3,4,5};

    // 获取数组第一项的内存地址并赋值给指针
    int *pointer = &arr[0];

    for ( int i = 0; i < 5; i++ ){
        cout << "TURN " << i+1 << endl; // 打印回合数
        cout << "pointer: " << pointer << endl;
        cout << "arr[i]: " << arr[i] << endl;
        cout << "*pointer: " << *pointer << endl;
        pointer++; // 相当于 pointer = pointer + 1,请注意这里的 1 是指 1 个 int 所占用内存的长度(4 字节)
    }

    return 0;
}

这里使用指针的加法遍历了一整个数组,运行结果(请注意看内存地址的差值,都是 int 的大小 4 字节,如果不会 16 进制的问某度):

 此外指针和指针还可以相减。这时候就有人问:为什么不能相加?很好解释,比如门牌号 32 和 30 相减代表它们之间隔着两户,而相加呢?什么意义都没有。也请注意得到的减值是指它们之间隔着两个 int 的大小,而不是隔着两个字节。

#include <iostream>
using namespace std;

int main(){
    int arr[5] = {1,2,3,4,5};

    // 获取数组第一项的内存地址并赋值给指针
    int *ptr1 = &arr[0];

    // 获取数组第三项的内存地址并赋值给指针
    int *ptr2 = &arr[2];

    // 获取它们相减的值并打印
    cout << "ptr2 - ptr1 = " << ptr2 - ptr1 << endl;
    
    return 0;
}
​

运行结果:

3. C++ 指针进阶

3.1 指针传参

传参,就是传递参数。讲指针传参之前,我们先要了解一下普通的传参方法的原理。函数传参,你可以把它理解为发信。调用函数,我们要把调用函数的要求和参数装进一个信封里,然后送进程序员给你预先准备好的邮局。邮局小哥会把你的参数 copy 一份,这是最关键的步骤,它防止了函数直接修改原变量。然后邮局小哥把复制品传入函数参数,函数开始运行。

普通传参全过程(绘图工具:Gitmind)

 但是指针传参就不一样了。指针传参,顾名思义,传的参数是指针。它规避了系统自动为你 copy 参数的特点,而是直接暴力地给你一个地址,你想要自己去找。但是地址无论怎么抄,指向的变量都是一样的,这就实现了函数内部直接更改参数原值。具体过程看下图。

指针传参全过程(绘图工具:Gitmind)

 话不多说我们来个栗子:

#include <iostream>
using namespace std;

// 交换两个参数原值
void swap(int *a, int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(){
    int num1 = 3;
    int num2 = 4;
    cout << "num1: " << num1 << "  num2: " << num2 << endl;

    // 指针传参
    swap(&num1, &num2); // 使用 & 取完的地址,其类型就是指针,可以开箱即用

    cout << "num1: " << num1 << "  num2: " << num2 << endl;

    return 0;
}

结果:

显而易见,函数通过左值解引用修改了指针指向的原值。

此外,还有一种传参方式和指针传参的效果一模一样,但写法更加简洁,那就是引用传参。引用传参除了得在参数名前面加上一个取地址运算符 & 以表明参数的类型是指针之外与普通传参一模一样,在函数中也不用刻意强调参数类型是指针。下面给出一个示例,运行结果如上图。建议大家平时需要用到指针传参的地方都使用引用传参代替,因为它更简洁,不过指针传参更好理解~

​#include <iostream>
using namespace std;

void swap(int &a, int &b){ // 引用传参需要在参数名前面加上 &,在函数中调用该参数和普通参数一模一样
    int temp = a;
    a = b;
    b = temp; 
}

int main(){
    int num1 = 3;
    int num2 = 4;
    cout << "num1: " << num1 << "  num2: " << num2 << endl;

    // 引用传参
    swap(num1, num2);

    cout << "num1: " << num1 << "  num2: " << num2 << endl;

    return 0;
}

3.2 const 指针

const 指针有两种:指向 const 对象的指针和地址被 const 的指针。

指向 const 对象的指针:这是 const 对象特需的一种指针,该指针不能通过左值解引用修改原值。当然这种指针也可以指向普通变量,只不过依然不能左值解引用。语法和栗子:

// 类型名 *const 指针名 = 指针值
// 比如:
const int num = 2;
int *const numPtr = &num

地址被 const 的指针:顾名思义该指针的地址被 const 了,也就是不能改动。所以它见到一个变量的地址就非他不指。当然它可以左值解引用。还有千万不要给这个指针赋值为 NULL,因为这样它就非 NULL 不指了,它就废了 QWQ。语法:

// const 指针类型 *指针名 = 值
// 热 fufu 的栗子
int num = 3;
const int *numPtr = &num;

栗子就别了吧。此外如果把上面两种 buff 叠加起来就会形成指向 const 对象的地址又被 const 的指针(emm,名字有点长)简单来说它既不能左值解引用也不能更改所指的地址,语法如下。但我个人觉得,这样的指针没有什么用……

const int num = 3;
const int *const numPtr = &num;

3.3 指针的数组和数组的指针

这一小节的标题是两个完全不同的概念,一个是数组,一个是指针。

先讲讲指针的数组。指针的数组,顾名思义它是由指针组成的数组。语法也很简单,只需要在数组名称前面加上 *,以表示它的元素是指针。

而数组的指针,顾名思义,它是一个指针,指向的数值是一个数组,一个完整的数组。它的语法就有点绕了,如下:

// 指针类型 (*指针名)[指向数组的长度] = 指向数组的地址

还有一种简便的方法来记忆:数组的指针比指针的数组多一个括号。

下面来个栗子:

#include <iostream>
using namespace std;

int main(){
    // 一个普通的数组
    int arr[5] = {0,1,2,3,4};

    // 数组的指针,指向数组 arr
    int (*arrPtr)[5] = &arr;

    // 指针的数组,数组里的每一项指向 arr 的对应项
    int *ptrArr[5] = { &arr[0], &arr[1], &arr[2], &arr[3], &arr[4] };

    cout << "arrPtr: " << arrPtr << endl;
    cout << "*arrPtr: " << *arrPtr << endl;
    for ( int i = 0; i < 5; i++){
        cout << ( *arrPtr )[i] << " "; // *arrPtr 虽然显示着只是一个地址,但是它和所有数组一样可以下标取值。这是编译器的问题。
        cout << ptrArr[i] << " ";
        cout << *( ptrArr[i] ) << endl;
    }
}

运行结果:

细心的同学肯定发现了,arrPtr 与 *arrPtr 的值一样。其实这是编译器的问题。同学们只需要知道 *arrPtr 和所有数组一样可以下标取值就行了。详细的原理引用我书里的一段话:

这里比较不直观的一点 arrPtr 和 *arrPtr 代表的地址完全一样,为了解释这一点,再看一个示例:

​
#include<iostream>
using namespace std;

//数组的指针的地址
//Author: 零壹快学

int main()
{
    int arr1[5] = { 0, 1, 2, 3, 4 };
    //数组的指针
    int (*arrPtr)[5] = &arr1;
    cout <<"arrPtr: " << arrPtr << endl;
    cout <<"*arrPtr:" << *arrPtr << endl;
    int arr2[5] = { 0, 1, 2, 3, 6 };
    //数组的指针必须指向大小相同的数组
    arrPtr = &arr2;
    cout << "arrPtr: " << arrPtr << endl;
    cout << "*arrPtr:" << *arrPtr << endl;
    //数组的指针指向数组,而数组是不可修改的
    //*arrPtr = arr1
    return 0 ;
}

​

运算结果:

arrPtr: 0033F7C0
*arrPtr: 0033F7C0
arrPtr: 0033F798
*arrPtr: 0033F798

        我们可以看到,数组的指针必须指向相同大小的数组,如果arr2只有4个元素,第十四行的赋值就会产生编译错误,并且由于数组的指针指向是不可修改的数组,我们不能把*arrPtr作为左值修改。

        至于为什么arrPtr和 *arrPtr的地址一样,可以看做是编译器不得已的安排。数组名arr1代表着数组首元素的地址,而一般的变化量比如int1就放着一个数值,而&int1才放着int1 的地址。由于数组的这一特殊性,导致了&arr得到的数组地址与arr代表的数组地址是一样的,因此相应的arrPtr和 *arrPtr的地址也只能是一样的,*arrPtr也要搭配下标操作符才能取得数组的对应元素。

3.4 指针的指针

指针其实说白了也是变量的一种。那普通的变量有指针,指针为什么不能有指针?指针的指针其实和普通的指针没有什么大不同,就是多了一个 * 号。话不多说我们上例子~

#include <iostream>
using namespace std;

int main(){
    int num = 3;
    // 指针
    int *numPtr = &num;
    // 指针的指针
    int **numPtrPtr = &numPtr;

    cout << "num: " << num << endl;
    cout << "*numPtr: " << *numPtr << endl;
    cout << "numPtr: " << numPtr << endl;
    cout << "*numPtrPtr: " << *numPtrPtr << endl;
    cout << "numPtrPtr: " << numPtrPtr << endl;

    return 0;
}

运行结果:

4. 小结

其实作为一个前 Java 程序元来说,学指针真的很痛苦(T-T)但是当你学完之后,后面的函数,面向对象,模板等操作学习就会更轻松,它毕竟是 C++ 的核心~

还有一个指针写了 6000 多字我也很惊讶~

PS:下集来攻关 C++ 面向对象~

猜你喜欢

转载自blog.csdn.net/raspi_fans/article/details/123227334