指针

定义和基本操作

定义

使用int *p或者int* p来定义。这两种写法都是可以的,但是int* p 更便于理解(p是int*类型的变量),而int *p是推荐的写法,这是因为可以写成int (*p),却不可以写成(int*) p。所以,理解的时候都无所谓,但是书写的时候尽量将*和标识符放在一起写,避免出现奇怪的错误。

可以使用typedef来定义一个指针类型,帮助理解,比如说typedef int *Pointer;(当然也可以写成typedef int* Pointer;)。所以说各种基础类型的指针类型也是一个独立的类型,表示该变量作为内存地址,这个内存地址位置储存的是对应的基础类型。因此一个指针类型,包含的信息至少有:值(一个地址)、指向的基础类型、指向的类型占空间大小。

运算

指针可以赋值。&符号在这里作为取地址符,可以协助指针进行赋值,但是要注意不能将不同的基础类型地址赋值给指针,例如不能将一个double类型变量的地址赋值给一个int*类型的指针。而*除了在定义指针的时候,都是作为间接取内容符来使用。其实取地址符&的返回值类型就是对应基础类型的指针类型,所以如果像上面那样干,就会提示不能把double *类型的值赋值给int *类型。

对指针进行加减操作,实际上就是对地址做偏移。每次+1的时候偏移的字节数等于sizeof(基类型),这也就是用指针表示数组的基本原理。指针之间进行相加很少有意义,但是相减可以算出两个指针之间的偏移量。两个指针类型加减的结果是一个整型,而且不是活生生的字节数偏移量,而是等于**两个指针地址值之差再除以size(基类型)**,可以用来计算数组两位置之间的距离。

类型转换

隐式转换:所有指针类型都可以隐式转换为void *类型。另外一种情况就是派生类的指针类型可以隐式转换为基类的指针类型,同理派生类的引用类型也可以隐式转为基类的引用类型。这个和派生类对象可以转换成基类对象的道理类似。注意隐式转化的时候不会发生拷贝,也不会发生切片,可以理解为基类对象的指针只是单纯地指向派生类对象内部那个基类对象了。请看下例:

#include <iostream>

class Father{};

class Son: public Father{};

int main(){
    Son son;
    Father* father_p=&son; //可以,派生类的指针类型可以隐式转化为基类的指针类型
    
    Father father;
    Son *son_p=&father; // 不可以,基类的指针类型不能隐式转化为派生类的指针类型
}

但是其他大多数指针类型的转换都需要进行显式强制转换了。

强制类型转换可以改变指针的基类型。基类型一旦改变,之后的间接取内容操作,都是按照新的基类型读取和对待数据。void *类型是一个特殊的指针类型,它不记录基类型,唯一信息是值(地址)。因此如果要间接取地址,必须先强制转换为其他类型,否则计算机不知道该以何种基类型对待这个地址中的内容。例如:

#include <iostream>
using namespace std;

int main() {
    int num = 65;
    void *p = &num;// void *中不储存任何基类型信息,不能直接使用间接取内容。
    // &num是int *类型,在此会被隐式转换为void *类型,然后给p初始化。
    char *q = (char *) p; //强转
    cout<<(int)*q<<' '<<(int)*(q+1)<<' '
        <<(int)*(q+2)<<' '<<(int)*(q+3);//以字符对待这里储存的数据
    return 0;
}

另外比较特殊的一个是char *类型。在数组的内容中已经提到,cout会将char []char *类型的数据自动当做字符串处理,也就会输出一个字符串了。

常量指针、指针常量

如字面意思,常量指针是说,指针中指向的基类型数据是常量,也就是不能通过这个指针改变所指基类型数据,但是指针本身的值是可以变化的。相当于一个残废的指针(指向的内容只读)。定义的方式是在最前面加const,类似const int *p。很常见的应用是在函数传参的时候,如果不希望函数误操作修改了指针指向的内容,则参数应声明为常量指针。原则是,在能用常量指针的时候都尽量加上const,保证安全。

指针常量是说,指针本身是一个常量。指针的值一旦确定,就不能改变,指向其他地址。但是还可以通过指针间接取内容的操作修改指向基类型数据的值。这个指针还是个正常的指针,只不过地址被固定了。定义的方法是在标识符前面加const,类似int * const p

函数指针

定义

函数指针定义的时候要指定参数类型列表和返回值,一个函数指针除了记录函数在代码区的地址之外,还至少包含参数和返回值两个信息。因此,不能将返回值或者参数不匹配的函数赋值给函数指针上。

在C++中,一个函数指针的初始化或者赋值直接使用函数名作为右值就可以了。在C++中,函数名作为右值,编译器为隐式转为函数所在的地址。

例如

#include <iostream>
using namespace std;

int foo();
double goo();
int hoo(int x);

int main() {
// 给函数指针赋值
    int (*funcPtr1)() = foo; // 可以
    int (*funcPtr2)() = goo; // 错误!返回值不匹配!
    double (*funcPtr4)() = goo; // 可以
    funcPtr1 = hoo; // 错误,因为参数不匹配,funcPtr1只能指向不含参数的函数,而hoo含有int型的参数
    int (*funcPtr3)(int) = hoo; // 可以,所以应该这么写

    funcPtr1 = &foo; //可以,但是不加&也可
}

通过函数指针调用函数

可以用间接访问内容的*符号来取出函数指针中的函数,也可以直接把函数指针当成函数名来写。例如接上一个例子,(*funcPtr4)(4)或者funcPtr4(4)都是支持的。建议直接当函数名来使用,好理解不容易出错。

函数指针传参

函数指针作为参数声明的时候,也需要声明参数列表、返回值,并且传入的函数也需要这两样都对应。

这个很常见的用法就是在排序中,通常需要传入一个大小比较的方法来帮助排序。下面看一个配合模板完成一个灵活的冒泡排序算法的例子(来自知乎):

#include <iostream>
template <typename T>
bool ascending(T x, T y) {
    return x > y; 
}
template <typename T>
bool descending(T x, T y) {
    return x < y;
}
template<typename T>
void bubblesort(T *a, int n, bool(*cmpfunc)(T, T)){
    bool sorted = false;
    while(!sorted){
        sorted = true;
        for (int i=0; i<n-1; i++)
            if (cmpfunc(a[i], a[i+1])) {
                std::swap(a[i], a[i+1]);
                sorted = false;
            }
        n--;
    }
}

调用这个函数的时候传入的第三个参数只需要是自己定义的比较函数的函数名就可以了,比如bubblesort<int>(a, 8, ascending);

指针&数组

数组和指针有着共同的本质。一个数组类型的变量,其实大致就相当于一个指向第一个元素的指针,包含地址的值和基类型两个基本的信息。因此很多地方,数组变量和指针变量可以一视同仁地使用。

一维,这个比较简单。定义一个一维数组变量在java中写作int[] nums,在C++中写作int nums[]或者int *nums。用指针的写法除了不能通过typeof()得到数组总大小、也不能通过{}来初始化,其他使用和数组都基本一致。他们都可以用下标或者偏移量的两种方式访问后面的元素。例如:

#include <iostream>
using namespace std;

int main() {
    int nums[6] = {0, 1, 2, 3, 4, 5};
    int *ns = nums;//或者写作 int *ns=&nums[0];
    cout << *(nums + 1);
    cout << ns[1];
}

升降维。定义了一个二维以上的数组之后,通过重新定义指针(或数组)的方法,可以切入其中的任意一个维度,进行降维处理。同样的,如果拥有一段一维的数组空间,也可以通过定义指针(或数组)的方法,以高维的视角管理空间。记住一点即可,只有第一个维度被当做数组处理,剩下的维度都是不可见的,全部封装在数组的基类型中。

例如一个三维数组int a[4][3][2],应当看做长度为4的数组,每个元素是一个int[3][2]类型的数组。因此,完全可以用指针int[3][2] (*p) 来表示这个数组,并且用法和a完全一致。写成int[3][2] (*p) 是为了方便理解,按照C++里的规矩,转化成int (*p)[3][2]即可,注意*p要加括号,不然会被当成int*类型的指针数组。此时的指针,1个单位的偏移量就是3*2*4=24字节,当使用p[3][2][1]时,实际上是*(p+3)[2][1],这里p只负责找到第一维的偏移地址,剩下的中括号交给基类型的数组处理,有条不紊。事实上,每一层中括号也可以用间接取地址层层替换,例如a[3][2][1]的作用和*(*(*(a+3)+2)+1)一致。从这里也可以看出,定义数组的时候第一维(数组长度)是没有必要指明的,编译器不关心数组多长,C++也不会对数组的越界进行任何判断。

继续刚才的思路。无论多少维的数组,不过是以线性的一维形式储存的。因此,升降维完全可能实现。刚才的int a[4][3][2],可以被当做一个12*2的数组处理,只需要定义一个新的指针(数组):int (*p)[2]或者int b[12][2](12可不写)。

指针数组

这里值得是基类型为指针的数组了。定义时通过类似char *args[]的方式,注意不要给*args加括号,不然就成了多维数组。

指针数组的每一个元素是一个指针。

指针数组一个常见的应用就是,作为main函数的参数来接受命令行参数和环境参数。例如C++中可以这样写main函数:int main(int argc, char *argv[], char *env[]),argv就是一个char *类型的指针数组。例如:

#include <iostream>
using namespace std;

int main(int argc, char *argv[], char *env[]) {
    cout << argc << endl;
    cout << argv[0] << endl; //第一个命令行参数一定是程序的路径
    cout << argv[1] << endl; //从第二个开始是自定义输入的命令行参数
}

指针和结构

这里的结构可以是结构体、联合体、对象。使用一个指向结构的指针来访问结构内的成员变量和方法时,需要先用*简介取内容,然后在用.来使用内部的变量和方法。C和C++提供了->操作符,可以在指针上使用,两步合一步完成对成员变量和方法的访问。例如(*myStruct).num可以用myStruct->num替代。

在传入结构作为函数参数的时候,尽量使用指针或者引用的方法进行传参,避免不必要的拷贝消耗资源。

多级指针

指针指向地址处存放的数据基类型,不仅可以是那些常规的类型,也可以是另一个指针。这样嵌套形成的指针,就是多级指针。多级指针定义的时候会使用多个*进行定义,使用时也可以通过多个*解引用操作取得最终的内容。

例如下面是一个交换字符串的方法:

void swap(char **p1, char **p2)
{    char *tmp = *p1;
           *p1 = *p2;
           *p2 = tmp;
}

void main()
{    char *p1 = "abcd";
    char *p2 = "1234";
    cout << p1 << "     "  << p2 << endl;
    swap(&p1, &p2);
    cout << p1 << "     "  << p2 << endl;
}

动态变量

动态变量指的是,在运行时期才在堆中申请空间实例化的变量。由于变量实际上在堆中储存,需要栈中有一个指向其地址的指针才能够使用。

在堆中管理空间的方法有两套,分别是new&delete以及molloc&free。两者的区别有很多,但是抓重点的话有两点,new&delete会调用构造和析构函数但molloc&free不会,new成功后返回值类型是基类型的指针,而molloc返回void*类型的指针,还需要强转才能用。另外使用molloc是的参数是需要分配的字节数。

除此之外,new还可以像Java中那样,一次创建一个数组,delete也可以删除整个数组(依次调用析构函数)。

C++中实现单链表就是典型的运用动态变量的例子。

引用

在C++中,引用可以理解为一种特殊的指针,尽管有着很多不同。也可以理解为变量的别名。(非常量)引用在初始化的时候获得和初始化用的变量同样的地址,因此必须用左值进行初始化。对于常量引用,也可以使用右值初始化,但是

引用在定义时必须初始化,且不能更换指向的对象,类型和被指向的内容相同

引用在使用是不必通过*间接取内容,而是可以直接当做那个变量来使用。

引用就是为已存在的一个对象取了一个新名字,而且不能是刚刚新创建的对象,例如A& a=A()不可以,这是因为引用赋值的时候要求等号右边是一个左值。但是可以使用常量引用const A& a = A()因为常量引用可以用临时变量赋值。

引用最多应用在函数传参的时候,可以避免拷贝。同时,引用也用来给动态参数命名。例如:

#include <iostream>
using namespace std;

void plus_one(int &num) {
    num++;
}

int main() {
    int &n = *(new int);
    n = 2;
    cout << n; //输出2
    plus_one(n); //不拷贝,而是传递一个“指向同一变量”的引用
    cout << n; //输出3
}

最后,如果函数的返回值是指针或者引用类型,则不能是一个在栈中的局部变量,因为函数调用结束后栈中局部变量的空间被释放,引用和指针就失去了意义。

通过auto声明变量的时候,auto会被自动推理为没有引用的原始类型。auto& 才表示要生命引用类型。换句话说单单auto编译器不会当做引用类型处理,而是会新建一个变量,而auto&会告诉编译器这是一个引用(别名),不需要新建变量。