0%

C++之指针

很多程序开发语言都不支持指针, 尤其在面向对象思想中, 指针被认为是应当禁止的, 因为它显然与面向对象世界格格不入. 然而尽管如此, C++还是从C语言中继承了指针, 这也是C++收到最多争议的地方.

指针提供了直接操作内存的手段, 但是这在某种程度上也增大了风险, 内存错误往往会导致严重的后果却难以在开发过程中察觉.

指针的概念

程序的变量和代码都存储在内存中. CPU执行的机器码通过内存地址来操作内存中的数据. 编译器的工作就是将程序中的变量和操作翻译成对内存地址进行操作的机器语言. 而在最终的机器码里其实是不存在变量名或变量类型的.

内存就是一段连续的存储空间, 数据在内存中按一定顺序连续存放. CPU中对内存中的数据进行操作首先需要通过某种寻址方式来定位并获取数据. 常见的寻址方式包括: 立即寻址, 直接寻址, 间接寻址,基址寄存器寻址, 变址寄存器寻址等.

一个变量的地址被称为该变量的”指针”. 如果定义一个变量专门用来存放另一个变量的地址, 则这个变量就是一个指针变量.

指针的语法

C++中使用”*”来表示指针变量和其所指向的变量之间的关系. 例如:

int i = 0;
int *p = &i;

上面的代码表示指针变量p指向了一个变量i, *p就表示p所指向的变量, 也就是i. 一般指针变量的定义形式如下:

类型 *指针变量名;

如果需要修改指针所指向的内容, 可以使用地址符”&”, 如:

int i = 0;
int j = 0;
int *p = i;
p = &j

这段代码首先定义了一个指针变量p, 并使它指向变量i, 然后又修改它指向j. 其中int *可以看作为一种类型名, 表示p是一个int型的指针. 但是仅仅是可以看作, 比如下面的代码:

int *a , b , c;

上述定义中只有a是一个指针变量, b和c都是int型的变量.

此外, 在定义指针时必须指定其类型, 因为不同类型的变量在内存中所占据的空间是不同的, 这对于只针对移位操作具有很重要的意义. 对于上面代码中int型的指针p, p++的操作表示将指针p移动4字节. 如果类型不匹配很可能得到错误的数据或发生访问越界.

函数与参数传递

C++中每个函数有且仅有一个返回值, 返回值类型可以为空, 用void标识. 当返回值不为空时, 函数体中必须含有return语句,否则可以省略return语句.

C++中,函数的传参方式有三种: 值传递, 指针传递和引用传递.

当使用值传递时, 函数内部对复制参数, 函数操作的是参数的副本. 当形参的值在函数体内改变时, 实参的值并不会发生改变:

#include <iostream>
using namespace std;

void fun(int x)
{
    cout << x << endl;
    x++;
    cout << x << endl;
}

int main(int argc, const char * argv[]) {

    int x = 0;
    cout << x << endl;
    fun(x);
    cout << x << endl;
    
    return 0;
}

上面的代码输出:0 0 1 0.

当进行函数参数是一个指针变量时, 就是指针传递:

#include <iostream>
using namespace std;

void fun(int *x)
{
    cout << *x << endl;
    (*x)++;
    cout << *x << endl;
}

int main(int argc, const char * argv[]) {

    int x = 0;
    cout << x << endl;
    fun(&x);
    cout << x << endl;

    return 0;
}	

上面的代码输出: 0 0 1 1.

C++中的引用可以理解为变量的一个别名. 引用必须在声明时就进行初始化. 对引用的改变实际上就是对引用目标的改变. 引用运算符”&”只在声明时使用, 任何其他的”&”都是地址操作符.

引用不是值, 不占用内存空间, 声明引用时目标的存储状态不会改变.

指针和引用的区别主要有3点:

  1. 存在空指针, 但不存在指向空值的引用.
  2. 使用引用之前不需要判空, 而使用指针时必须判空.
  3. 指针可以被重新赋值, 引用在声明和初始化之后不能改变其指向的目标.

此外, 不能建议引用的数组. 因为数组是某个数据类型元素的集合, 数组名表示该元素集合空间的起始地址, 数据并不是一个数据类型. 引用本身也不是一个数据类型, 声明引用在概念上不产生内存空间, 因此也没有引用的引用, 也没有引用的指针.

#include <iostream>
using namespace std;

void fun(int &x)
{
    cout << x << endl;
    x++;
    cout << x << endl;
}

int main(int argc, const char * argv[]) {

    int x = 0;
    cout << x << endl;
    fun(x);
    cout << x << endl;

    return 0;
}

引用传递也用在传递大对象时, 当对象非常大时, 值传递的复制过程会消耗大量时间. 如果传递的引用并不需要被修改, 可以将引用声明为const类型. 例如:

void fun(const int &x){...}

此外, 可以采用引用传递方式来传递一个指针. 尤其是当函数需要改变指针所存储的内存地址时.

#include <iostream>
using namespace std;

void first_bigger(int *&p, int threshold)
{
    while (*p <= threshold) {
        p++;
    }
}

int main(int argc, const char * argv[]) {

    int nums[] = {0,12,32,44,55,66,77,88,99};
    int *result = nums;
    first_bigger(result, 50);
    cout << *result << endl;
    return 0;
}

上述代码是从一组数字中找出第一个大于参考值的数字. 函数的第一个参数是一个指针的引用, 并在函数体内修改了指针中存放的地址.