【洛谷日报#75】浅谈C++指针

zhouwc

2018-09-22 16:08:30

Personal

#### 放入我的[博客](https://csgblog.top/4ccf99cf81c42233c714e9064d32a457/c%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%9A%E6%8C%87%E9%92%88/)食用效果更佳(有很多oi学习资料) ## 1.指针基础 #### 1.引用 C++有一个东西叫**引用**,引用相当于给**对象**(如:变量)起了另一个名字,引用必须用对象初始化,一旦初始化,引用就会和初始化其的对象**绑定**在一起,就是说引用的值就是被引用的对象的值,引用的值被修改时被引用的对象也会被修改,但不能定义引用的引用,因为引用不是对象,引用定义方式:类型 &引用名1=对象名1 比如: ```cpp int i=0; int &x=i,y=i,z=i; ``` 只有x是引用且与i绑定,y和z都只是一个初值为0的int类型变量。 我们往往可以使用引用类型来简化代码或者节省空间复杂度。引用有些地方需要注意可以看下面代码: ```cpp int &ref=10; //错误,引用初始值必须是对象 double x=1.78; int &ref2=x; //错误,ref2的引用类型与x不匹配 double &p=x; //正确 p=3.14; ``` 如果去除错误项就运行上述代码,x的值就会变成3.14。 #### 2.基本的指针 然后C++里还有一个东西叫**指针**,是一种对象。 指针和引用定义类似,只不过把`&`改成了`*`符号,可以不初始化。指针在本质上是一个地址,因此指针的赋值需要用取地址符`&`(注意和引用要区分)。获取指针指向的地址有两种方式,这里先介绍一种,用`*指针名`来获取指针指向的地址的变量的一个引用。举个例子: ```cpp int i=25; int* x=&i; int y=*x; (*x)=6; ``` 运行完上述代码后,i的值变成了6。 和引用相同,指针指向的类型必须与赋值给其的地址类型匹配。指针一般有三种形式: 1.指向一个对象 2.空指针 3.无效指针 空指针即指针值为NULL,这个东西定义在头文件cstdlib中。如果指针未被初始化或者指向对象的空间被回收等等则该指针为无效指针。我们应尽量避免出现无效指针,这往往会让你的代码出现错误而且难以调试。 指针可以使用加减运算符,表示向前或向后任意单位长度的对象的地址。 因为指针是对象,所以可以有指针的指针,指针的指针称为二级指针,二级指针的指针称为三级指针,三级指针的指针称为四级指针,以此类推。指针的功能十分强大,但也难以调试,很多程序员往往会在调试指针上花费大量时间。 ## 2.指针拓展 #### 1.引用,指针与常量 这是比较搞脑子的一块内容,请做好准备。 引用与常量的关系只有一中:**常量引用**,即引用本身的类型必须是常量,但引用的可以是常量也可以是对象。 ```cpp 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的值 ``` 因此我们在函数中传递某些参数时可以使用常量引用来减少空间复杂度。 指针与常量的关系有两种情况,一种与引用类似,称为**指向常量的指针**,本身类型必须是常量,但指向的值不一定是常量,只是无法通过该指针来修改值,常量指针可以改变其指向的地址。如: ```cpp const int i=0,j=0; const int *p=&i; p=&j; //正确 ``` 另一种称为**常量指针**,这种指针必须初始化,且指向的地址初始化后不能被改变,这种指针的定义有点不同,需要这么定义: 类型 `*const 指针名` 如: ```cpp 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,可以这么开: ```cpp int _a[20]; int *const a=&_a[10]; ``` 或者这样也可以:`int _a[20],*const a=_a+10;`然后我们就可以使用一个下标为可以为负数的a数组了。 同样,在传递参数时,传递整个数组不方便,我们可以把它作为指针来传递,这样做在修改时会直接在你原先的数组中修改。比如: ```cpp void func(int* a){ //内容 } ``` 同理,交换两个数组也可以用类似的方式。但是由于直接开出来的数组用的是常量指针,无法交换,所以我们可以用普通的指针去指向它们,然后交换普通的指针。 ```cpp int _a[100],_b[100]; int *a=_a,*b=_b; //使用普通指针代替 for(int i=0;i<4;++i) a[i]=1; std::swap(a,b); for(int i=0;i<4;++i) printf("%d",b[i]); ``` 上面这段代码就会输出1111。 #### 3.指针与多维数组 多维数组就是多级指针,但是系统并不知道你一个指针所指向的区域有多大,所以作为参数向函数传进去时需要表明你的数组大小(注意:如果填的数组大小一定要与实际的相符)。同样,在函数中不会另开一个数组,如果出现了修改,会直接在你传进去的多维数组中修改。举个例子: ```cpp int b[14][20][20]; void func(int a[14][20][20]){ a[0][0][0]=1; } int main(){ func(b); printf("%d",b[0][0][0]); } ``` 这段代码会输出1。 如果要在函数中返回多维数组,同样可以用指针。C++规定函数不可返回数组,所以我们需要将其调typedef之后以指针的形式返回。 ```cpp typedef int A[20]; int a[20][20]; A* func(){ //内容 return a; } ``` 注意,这种方式不可返回函数中开的数组,因为在函数结束时该内存空间会被销毁,此时返回的指针会指向未知内存区域。 #### 4.指针与函数 我们可以使用指针来换函数名,这样的指针称为**函数指针**。函数的类型是由其返回值和参数决定的,因此我们需要这样定义一个指向函数的指针: 函数返回值类型 (`*函数指针`)<函数参数表> 先写一个这样的函数 ```cpp void func(const int &x){ //内容 } ``` 主程序里可以这么写: ```cpp void (*p)(const int&); //定义函数指针 p=&func; //将函数指针p指向函数func (*p)(4); //调用p指向的函数 p(4); //这是一个与上一行等价的调用 ``` 函数指针是一个对象,因此我们可以像传变量一样把它传来传去,但是往往需要类型别名。如: ```cpp typedef void (*F)(const int&); void func(const int &x){ //内容 } F func2(F p){ return p; } ``` 这样我们就可以像sort函数那样传个cmp函数之类的来使代码功能更多。 ## 4.指针的相关应用——链表 #### 1.动态内存 C++语言中,我们可以使用new语句来在系统堆空间中开出点空间来并返回地址,我们可以使用指针来存储开出来的地址。如: ```cpp int* x=new int; //x指向了一个未初始化的int类型变量 ``` C++还支持开动态的一维数组: ```cpp int* x=new int[10]; //此时x是指向一个大小为10的数组的下标为0的位置的指针 ``` 特别的是,动态一维数组的下标范围可以不是常量表达式。如果要开多维的,就有些麻烦了,以二维为例,开一个大小为n×5的数组就需要这么开了: ```cpp int** x=new int*[n]; //注意此时x是一个二级指针 for(int i=0;i<n;++i) x[i]=new int[5]; ``` 工程上,使用new语句有时会出现一些鬼畜的错误,我们就需要使用一些其它的东西,比如说这个定义在头文件new中的nothrow对象,我们可以这么写: ```cpp int* x; x=new (nothrow) int; //如果分配出错,x就会变成空指针 ``` 既然是动态内存,我们当然可以随时释放它。释放要使用delete表达式,形式是: ```cpp delete p; //其中p一定要是一个指针,会把p指向的动态内存释放掉 ``` 注意,这里p指向的一定要是一个new语句开出来的内存的地址,不然会出错。执行该语句后,p变成了空悬指针,是一种无效指针。为了避免这类无效指针再来出一些奇奇怪怪的错误,我们可以把它变成空指针。 #### 2.链表 如果要使用纯正的C++链表,我们需要使用结构体(或者类)嵌套定义并用动态内存去使用。比如,我们可以定义一个双向链表的结点: ```cpp struct Node{ Node* next; Node* pre; int key; }; ``` 然后我们可以使用动态内存去开结点,更改结点。我们会发现,很多时候都要用到类似与(*x).y的形式,非常不方便,C++给出的一种简便写法,就是x-&gt;y,这可以使你的链表更加简洁。一下是一段依次读入n个数然后输出的代码: ```cpp #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++中重要的一部分,用得好可以使你的代码更简洁,运行更快,功能更多,而动态内存和相关的链表速度有些慢,不建议在竞赛中使用。指针主要是可以实现一些对于内存空间的操作,这个概念有些抽象,所以当指针出错时,其调试难度也将大大增加。所以对于OIer们,我不建议大量使用指针(特别是在不是模板的代码中),在java等较新的语言中,有相当一部分使用了面向对象中的动态绑定替代了指针的使用。