浅谈C++指针

1.指针基础

1.引用

C++有一个东西叫引用,引用相当于给对象(如:变量)起了另一个名字,引用必须用对象初始化,一旦初始化,引用就会和初始化其的对象绑定在一起,就是说引用的值就是被引用的对象的值,引用的值被修改时被引用的对象也会被修改,但不能定义引用的引用,因为引用不是对象,引用定义方式:

类型  &引用名1=对象名1

比如:

int i=0;
int &x=i,y=i,z=i;

只有x是引用且与i绑定,y和z都只是一个初值为0的int类型变量。

我们往往可以使用引用类型来简化代码或者节省空间复杂度。引用有些地方需要注意可以看下面代码:

int &ref=10;      //错误,引用初始值必须是对象
double x=1.78;    
int &ref2=x;      //错误,ref2的引用类型与x不匹配
double &p=x;      //正确
p=3.14;

如果去除错误项就运行上述代码,x的值就会变成3.14。

2.基本的指针

然后C++里还有一个东西叫指针,是一种对象。
指针和引用定义类似,只不过把&改成了*符号,可以不初始化。指针在本质上是一个地址,因此指针的赋值需要用取地址符&(注意和引用要区分)。获取指针指向的地址有两种方式,这里先介绍一种,用*指针名来获取指针指向的地址的变量的一个引用。举个例子:

int i=25;
int* x=&i;
int y=*x;
(*x)=6;

运行完上述代码后,i的值变成了6。
和引用相同,指针指向的类型必须与赋值给其的地址类型匹配。指针一般有三种形式:
1.指向一个对象
2.空指针
3.无效指针
空指针即指针值为NULL,这个东西定义在头文件cstdlib中。如果指针未被初始化或者指向对象的空间被回收等等则该指针为无效指针。我们应尽量避免出现无效指针,这往往会让你的代码出现错误而且难以调试。
指针可以使用加减运算符,表示向前或向后任意单位长度的对象的地址。
因为指针是对象,所以可以有指针的指针,指针的指针称为二级指针,二级指针的指针称为三级指针,三级指针的指针称为四级指针,以此类推。指针的功能十分强大,但也难以调试,很多程序员往往会在调试指针上花费大量时间。

2.指针拓展

1.引用,指针与常量

这是比较搞脑子的一块内容,请做好准备。
引用与常量的关系只有一中:常量引用,即引用本身的类型必须是常量,但引用的可以是常量也可以是对象。

const int i=5;
const int &r1=i;       //正确,i是常量
const int &r2=15;      //正确,15是常量
int i=42;
const int &r=i;   //正确,r绑定了变量i,引用r不能修改i的值
r=0;   //错误,不能用r来修改i的值

因此我们在函数中传递某些参数时可以使用常量引用来减少空间复杂度。
指针与常量的关系有两种情况,一种与引用类似,称为指向常量的指针,本身类型必须是常量,但指向的值不一定是常量,只是无法通过该指针来修改值,常量指针可以改变其指向的地址。如:

const int i=0,j=0;
const int *p=&i;
p=&j;   //正确

另一种称为常量指针,这种指针必须初始化,且指向的地址初始化后不能被改变,这种指针的定义有点不同,需要这么定义:
类型  *const  指针名
如:

int k=12;
int *const p=&k;
const double d=1.5;
const double *const p2=&d;  //p2是一个指向常量的常量指针

2.指针与数组

这里我们要介绍指针的第二种获取对应地址的变量的引用的方式:下标运算符。下标运算符可以获得指针指向位置往后任意个单位的地址的引用。这让大家想到了什么?数组!没错,数组本质上就是在系统栈空间中开出了一块连续的空间,然后使用数组时数组就是一个指向该数组第0位的常量指针,我们可以用星号来得到第0位的地址的引用,而二维数组就是一个二级指针,三维数组就是三级指针!
知道了这些,我们就可以开挂了。有些人或许在抱怨C++中用不了下标为负数的数组,其实这是可以的。比如我们想开一个下标为[-10..9]的数组a,可以这么开:

int _a[20];
int *const a=&_a[10];

或者这样也可以:int _a[20],*const a=_a+10;然后我们就可以使用一个下标为可以为负数的a数组了。
同样,在传递参数时,传递整个数组不方便,我们可以把它作为指针来传递,比如:

void func(int* a){
    //内容
}

还有可以使用指针来实现循环数组的交换等等。但要注意的是,像vector这样的容器并不是指针。

3.指针与函数

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

void func(const int &x){
    //内容
}

主程序里可以这么写:

void (*p)(const int&);   //定义函数指针
p=&func;                 //将函数指针p指向函数func
(*p)(4);                 //调用p指向的函数
p(4);                    //这是一个与上一行等价的调用

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

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

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

3.指针的相关应用——链表

1.动态内存

C++语言中,我们可以使用new语句来在系统堆空间中开出点空间来并返回地址,我们可以使用指针来存储开出来的地址。如:

int* x=new int;  //x指向了一个未初始化的int类型变量

C++还支持开动态的一维数组:

int* x=new int[10];  //此时x是指向一个大小为10的数组的下标为0的位置的指针

特别的是,动态一维数组的下标范围可以不是常量表达式。如果要开多维的,就有些麻烦了,以二维为例,开一个大小为n×5的数组就需要这么开了:

int** x=new int*[n];   //注意此时x是一个二级指针
    for(int i=0;i<n;++i) x[i]=new int[5];

工程上,使用new语句有时会出现一些鬼畜的错误,我们就需要使用一些其它的东西,比如说这个定义在头文件new中的nothrow对象,我们可以这么写:

int* x;
x=new (nothrow) int;  //如果分配出错,x就会变成空指针

既然是动态内存,我们当然可以随时释放它。释放要使用delete表达式,形式是:

delete p;   //其中p一定要是一个指针,会把p指向的动态内存释放掉

注意,这里p指向的一定要是一个new语句开出来的内存的地址,不然会出错。执行该语句后,p变成了空悬指针,是一种无效指针。为了避免这类无效指针再来出一些奇奇怪怪的错误,我们可以把它变成空指针。

2.链表

如果要使用纯正的C++链表,我们需要使用结构体(或者类)嵌套定义并用动态内存去使用。比如,我们可以定义一个双向链表的结点:

struct Node{
    Node* next; Node* pre; 
    int key;
};

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

#include<cstdio>
struct Node{
    Node* next;  
    int key;
};
int main(){
    Node* head=new Node;
    Node* x=head; 
    int n;
    scanf("%d%d",&n,&head->key);
    for(int i=1;i<n;++i){
        x=x->next=new Node;
        scanf("%d",&x->key);
    }
    x->next=NULL,x=head;
    for(int i=0;i<n;++i){
        Node* y=x;
        x=x->next;
        printf("%d ",y->key);
        delete y;
    }
    return 0;
}

4.总结

指针是C语言的灵魂,也是C++中重要的一部分,用得好可以使你的代码更简洁,运行更快,功能更多,而动态内存和相关的链表虽然速度有些慢,但可以很好的支配空间。

 

 

 

1 thought on “浅谈C++指针”

Leave a Comment

电子邮件地址不会被公开。 必填项已用*标注