指针

· · 个人记录

写在前面

说实话,指针这个东西我是真滴不想学。平时用用struct写算法竞赛的代码也够了,况且指针调试起来着实麻烦。但是在某神(@MarsCain)的飞速学习进度下,我也不得不开始学这玩意了。

因为手边没有专门写c++的书(不买c++ primer plus/effective c++/essential c++的我是屑),所以主要资料来自洛谷日报#75。(只是自己写写笔记的话应该不会侵权8)

引用和指针

引用和指针在定义初始化时的目标都必须是对象(毕竟面向对象编程)
定义时,引用使用符号 &a,指针使用符号 *a

int i = 1;
double pi = 3.14;
int &r1 = i;  //定义引用,必须让他引用一个同类型对象,实现绑定
int &r1 = 1;  //错误,对象不能是一个数值
int &r1 = pi; //错误,类型不同
int *r2 = &i; //定义指针,必须让他指向一个地址,此处&是取地址符
int *r2 = 1;  //错误,1不是一个地址
int *r2 = i;  //错误,i不是一个地址
int *r2 = π//错误,类型不同

当建立起引用或指针的关系后,他们与其指向的对象相互绑定,任意一方发生数值变化,都会引起两者的改变

int i = 1;
int &r1 = i;
int *r2 = &i;
r1 = 14 //错误,r1作为一个指针,本身是一个地址,需要写成*r1才能获得对象,如下
*r1 = 14; cout << r1 << " " << *r1 << " " << i << endl; //0x1027880d0 14 14
r2 = 130; cout << r2 << " " << i << endl;               //130 130

从上述代码中可以看出,引用是给一个地址多种不同的变量名来表示
指针是定义一个对象来存储地址,地址上的值通过*来获取
(且引用不能套娃,但指针可以)

指针

指针也有三个分类

  1. 指向一个对象
  2. 空指针
  3. 无效指针

空指针即NULL,定义在cstdlib中
当指针未被初始化或指向对象的空间被回收时,即为无效指针(野指针)

常量

引用与常量的互动称为常量引用

const int i = 1;
const int &r1 = i;
const int &r1 = 1; //这两种定义方式都是正确的
int &r2 = i;       //错误,引用必须也是常量引用
i = 2;             //错误,因为是常量
r1 = 2;            //错误,因为是常量

int i = 1;
const int &r1 = i;
r1 = 2; cout << r1 << " " << i << endl; //错误,r1是常量不能改变
i = 2;  cout << r1 << " " << i << endl; //2 2

指针与常量的互动有指向常量的指针,常量指针两种

指向常量的指针

与常量引用相似的称为指向常量的指针
但它可以改变自己所指向的地址(换个常量指着)

const int i = 1, j = 2;
int k = 3;
const int *r1 = &i;
cout << *r1 << endl;           //1
r1 = &j; cout << *r1 << endl;  //2
r1 = &k; cout << *r1 << endl;  //3
*r1 = 4;                       //错误,因为是常量

常量指针

常量指针指向的地址初始化后不能被改变,且它必须被初始化
而且在定义时的语法也有所不同: 类型 *const 指针名
(同样可以实现套娃)

const int i = 1;
int j = 2;
int *const r1 = &i;       //错误,因为这个指针类型是int,并不是常量
const int *const r1 = &i; //正确
int *const r1 = &j;       //正确

一维数组

众所周知,开一维数组是定义起始位置和长度,即

&a[5] == a + 5;
a[5] == *(a + 5);

其中a = a + 0,也就是说a本身就是一个地址,所以在输出时中括号的语法省略了*
这个性质有什么用呢?可以拿来开负数数组呀。

int a[10];
int *const r1 = &(a[5]);
for(int i = 1; i < 10; ++i) a[i] = i;
cout << r1[-4];    //1
cout << *(r1 - 4); //1

同时,在定义函数时使用引用,可以从传值变为传址 使用指针,可以方便快速传递数组

void swap1(int a, int b){swap(a, b);}
void swap2(int &a, int &b){swap(a, b);}
void swap3(int *a, int *b){int c;c = *a; *a = *b; *b = c;}
swap1(a, b);cout << a << " " << b << endl;   //1 2
swap2(a, b);cout << a << " " << b << endl;   //2 1
swap3(&a, &b);cout << a << " " << b << endl; //1 2
//swap3的情况说明了虽然传递了地址,但仍只是在临时变量中交换了地址,地址上的值没有发生改变。

多维数组

多维数组就是多级指针,作为参数向函数传进去时需要表明你的数组大小
注意:填的数组大小一定要与实际的相符(只有第一维可以用空的[]代替,因为中括号不就是指针吗XD),如果不符的话编译可能会通过并运行成功,但这极具危险性。
因为是指针传递,修改是在原地址进行的。

int b[4][4][4];
void func(int a[4][4][4])
{
    for(int i = 1; i <= 3; ++i)
        for(int j = 1; j <= 3; ++j)
            for(int k = 1; k <= 3; ++k)
                a[i][j][k] = i * j * k;
}
int main()
{
    func(b);
    for(int i = 1; i <= 3; ++i)
    {
        for(int j = 1; j <= 3; ++j)
        {
            for(int k = 1; k <= 3; ++k)
                cout << b[i][j][k] << " ";
            cout << endl;
        }
        cout << endl;
    }
    return 0;
}

output:
1 2 3 
2 4 6 
3 6 9 

2 4 6 
4 8 12 
6 12 18 

3 6 9 
6 12 18 
9 18 27

如果要在函数中返回多维数组,同样可以用指针。C++规定函数不可返回数组,所以我们需要将其调typedef之后以指针的形式返回。

typedef int A[20];
int a[20][20];
A *func()
{
    //内容 
    return a;
    //众所周知只要在数组后加一个指针就是二维数组了呢
}

注意,这种方式不可返回函数中开的数组,因为在函数结束时该内存空间会被销毁,此时返回的指针会指向未知内存区域。

typedef

因为没用过,来点知识补充 typedef的作用一个是给变量一个更有意义类型名字,另一个是简化一些比较复杂的类型声明。
(不过说实话需要锻炼记忆能力)(代 码 谜 语 人)

typedef int a;     //此后a = int,后续可使用 a d = 10;
typedef int b[40]; //此后定义b e; e就是一个有40个元素的数组
typedef int *c;    //

typedef struct tagMyStruct
{
    int iNum;
    long lLength;
}MyStruct;//这时我声明了tagMyStruct这个类型,同时定义了struct tagMyStruct这段话为MyStruc
          //C语法,c++定义变量时前面已经不需要加struct了,所以没啥用

结构体

有指针之后就可以实现很多莫名其妙的功能惹

struct node
{
    int num;
    struct node *lson,*rson;//指向自己类型的指针,可以画链表和树
};

struct B
struct A
{
    struct B *partner;
};
struct B
{
    struct A *partner;//两个结构体互指,我写线段树维护树剖的时候好像会用,减少全局定义数量和重名的问题
};

函数

指针也可以指向函数,这样的指针称为函数指针。函数的类型是由其返回值和参数决定的,因此我们需要这样定义一个指向函数的指针: 函数返回值类型 (*函数指针) <函数参数表>

函数指针的括号不能省略,否则会变成返回值是指针的函数
函数指针的类型定义必须于指向的函数类型相同
说点简单的,函数和变量一样都有一个地址,从某个地址入口进入就可以执行函数,所以可以让一个指针指向这个地址来调用函数

void (*p)(const int&);   
p = &func;               
(*p)(4);                 
p(4);                    //这是一个与上一行等价的调用
int func(int x){return x + 1;}
int main()
{
    int (*p)(int x);     //定义函数指针
    p = func;            //将函数指针p指向函数func
    p = &func;           //另一种写法
    cout << p(4) << " "; //调用p指向的函数
    cout << (*p)(4);     //另一种写法
    //5 5
    return 0;
}

函数指针是一个对象,因此我们可以像传变量一样把它传来传去,但是往往需要类型别名。如:

typedef void (*F)(const int&);
void func(const int &x)
{
    //内容
}
F func2(F p)
{
    return p;
}

这样我们就可以像sort函数那样传个cmp函数之类的来使代码功能更多。

动态内存

有了指针,我们就可以使用动态内存了

int *r1 = new int;
int *r2 = new int();
int *r2 = new int(5);
cout << *r1 << " " << *r2 << " " << *r3 << endl; //rand() 0 5

这里r1并没有被初始化,也就是说它是一个随机数;r2被初始化为0
注意,为了防止指针变成野指针出现莫名其妙的错误,可以使用nothrow:new (nothrow) int
当内存分配失败时,new只会抛出异常,有nothrow时会返回一个空指针 有分配就应该释放:delete r1; r1 = NULL;(r1一定是一个指针)
释放内存只是将该部分内存释放,上面的数据并没有擦除,可能存在残留。通过赋值NULL使其完全释放。

避免野指针,从我做起!

当然还有数组版本

int *r1 = new int[];
int **r2 = new int*[];   //r2是一个二级指针
    for(int i = 0; i < n; ++i) r2[i] = new int[];
delete [] r1; r1 = NULL;
delete [] r2; r2 = NULL; //目前看来这样可以释放全部内存

链表

(因为原文太好懂了懒得写代码这段我直接抄来了,浅改码风)
然后我们可以使用动态内存去开结点,更改结点。我们会发现,很多时候都要用到类似与(*x).y的形式,非常不方便,C++给出的一种简便写法,就是x->y,这可以使你的链表更加简洁。一下是一段依次读入n个数然后输出的代码:

#include <bits/stdc++.h>
using namespace std;
struct node
{
    node *next;  
    int key;
};
int main()
{
    node *head = new node;
    node *x = head; 
    int n;
    cin >> n >> head -> key;
    for(int i = 1; i < n; ++i)
    {
        x = x -> next = new node;
        cin >> x -> key;
    }
    x -> next = NULL; x = head;
    for(int i = 0; i < n; ++i)
    {
        node *y = x;
        x = x -> next;
        cout << y -> key;
        delete y; y = NULL;
    }
    return 0;
}