C/C++指针和引用详解

来源:互联网 发布:如何改变图片尺寸软件 编辑:程序博客网 时间:2024/04/29 16:55

前言

很多学习C/C++刚接触指针和引用的同学都不能很好地理解这两个概念,这篇文章主要介绍 C/C++指针和引用 的常规用法。

如有错误,希望各位读者批评指正!



指针和引用

指针的内存结构

所有的指针(不考虑C++中Boost封装的指针和STL的迭代器,这里只讨论最基本的指针)都有相同的内存结构, 一个定长的内存区域 ,在这个区域中有一个 内存地址。在32位平台上,这个大小一般是4字节(理论上由编译器配置决定)。

我们可以将指针内部存放的地址看成一个 无符号整数,表示了该指针指向的内存单元编号。如果该对象占多个连续内存单元,则指向该对象所占用的第一个内存单元,即 首地址


指针的类型

一般情况下,指针的类型很好判断。比如有以下声明:

int a = 12;int *b = &a;

我们称变量b是一个 指向int类型的指针。这个声明传达了两个信息:

  • b是一个指针
  • b指向的内存区域应被视为一个int类型的变量

这里的&符号是对a取地址,而a的类型是int类型,经过取地址操作后产生int类型的指针。

再举个例子:

int **c = &b;

直接去掉离变量最近的一个*就可以得到指针的类型,所以c是一个 指向int*类型的指针b的类型是int类型的指针,经过取地址操作后生成 int类型的指针 的指针,表示了b这个指针本身所占用的内存单元,将这个地址赋值给c

我将不同的内存单元用不同的颜色表示,绘制了如下的示意图,希望能帮助您理解。
示意图


指针的初始化

在声明指针后立即初始化是一个很好的习惯。如果暂时不需要对这个指针赋值,一般的处理方法是把这个指针赋值为NULLNULL是一个宏定义,在预编译阶段会被编译器替换成0(一个约定的没有意义的指针)。

请注意,以下两种写法是 等价 的。指针声明中的*和对指针取指向对象的*是完全不一样的。

int a = 12;int *b = &a;
int a = 12;int *b = NULL;b = &a;

指针的声明细节

在指针声明中,可能会遇到若干个*连在一起的情况,如int ***a;,这个声明与(((int*)*)*) a;是等价的:表示a是一个指针,指向的地址域应被识别为((int*)*)类型。

如果几个不同类型的变量具有相同的 最终对象类型 ,那么是可以在同一个声明语句中声明的;而且由于声明语句的解析顺序从左向右,所以从语法角度讲也可以在声明一个变量后随后声明其指针。

比如:

int a, *b, *c = &a, **d = &c, &e = a, f[12][13] = {0};

上述声明与下面的声明是等价的:

int a;int *b;int *c = &a;int **d = &c;int &e = a; // 这是引用,将在后面讲解int f[12][13] = {0}; // 用初始化列表初始化的数组

但是一般不推荐在同一行同时声明一个类型的不同形态指针或引用,因为这样很可能会在一定程度上降低代码的可读性。所以上述第一种代码尽量不要使用,但是如果看到别人这样写应该可以理解其含义。


指针的运算

指针的运算有两种:

  • 原始指针(X类型指针) + 偏移量(整数) = 移位后的指针(X类型指针)
  • 指针A(X类型指针) - 指针B(X类型指针) = B到A的偏移量(整数)

请注意:这里的偏移量 并不是以字节为单位的,而是以该 指针指向对象的内存占用大小 为单位的。 执行两种运算操作后的结果均为临时结果,若不使用将丢失(类比整数a + b)。

举个例子:

int a;printf("0x%08X\n", &a);printf("0x%08X\n", &a + 1);

执行以上代码你会发现:输出的两行地址,第二行总比第一行的数字多4。这是因为int在你的编译环境下占4字节,而指针的+1操作在原始地址的基础上增加了一个int类型的大小。

再举个例子:

int a[10];printf("%d\n", &a[5] - &a[0]);

你将会得到5的输出。

参与减法操作的指针类型必须相同,否则不能通过编译。

顺便说一句:这个运算规则在可随机访问的STL容器的迭代器之间也是成立的(如vectordeque等)。


使用typeid获取对象类型

在讲解数组指针之前,我推荐大家先了解一下typeid

typeid是C++中动态识别变量类型的一个类,我们暂时不需要考虑这个类的底层实现,只需要了解一个用法即可:

typeid(a).name();

这句话能直接返回a的类型的字符串表示。

为方便大家理解数组的指针,我编写了下面的函数。这个函数可以接收任意类型的参数然后将该类型对应的标识符打印出来。大家可以将这段代码复制到源文件中,不需要理解其原理。

#include <typeinfo>#include <cstdio>#define printID(x) _printID(x, #x)template<typename T>void _printID(T var, const char *name) {    printf("%s: %s\n", name, typeid(var + 1).name());}

使用时直接调用printID即可,如主函数内这样写:

int *a;printID(a);

运行时会显示:

a: Pi

说明了变量a的编译器内部表示是Pi,其中P代表指针,i代表int
由于有些指针有在编译器内部表示中有一些特殊的标识,为了能 暴露出这些指针最本质的属性,我在上述代码中使用了typeid(var + 1).name(),虽然不是一个很优雅的做法,但却是可以达到目的的。(如果您的输出与我不同,属于正常现象,因为这个函数是依赖编译器的,但也应该可解读的)


数组指针

在数组声明后,数组的名字可以直接作为一个指针,指向数组在内存中的起始位置。

执行以下代码:

int a[10];printID(a);

你会得到输出:

a: Pi

int*是完全一样的类型。请注意,在这里识别的一样的类型就说明一维数组名称所对应的指针类型和该类型指针是完全相同的。

现在来看二维数组:

int b[10][11];printID(b);

执行上述代码将会得到以下输出:

b: PA11_i

这个输出是什么意思呢?

实际上,P还是指针的意思,A11代表指向类型是一个长度为11的数组,_i说明数组元素的类型是int

下面我们看一下如何给一个函数传入一个二维数组的参数。
也许大家会写出这样的代码:

void fun(int **x) {    printID(x);}int main() {    int b[10][11];    printID(b);    fun(b);    return 0;}

上述代码在编译阶段会收到错误信息。在GCC环境下收到的信息是:

error: cannot convert ‘int (*)[11]’ to ‘int**’ for argument ‘1’ to ‘void fun(int**)’

只是因为传入的参数和预期收到的参数类型不匹配。

那么int**是一个什么类型呢?做一下测试,你会知道int**的类型是PPi

事实上,由二维数组名代表的指针并不是一个“指针的指针”,而是一个“数组的指针”。所以fun函数传入的参数类型应该是x的声明类型int[10][11],这里的10可以省略,因为我们并没有在标识里看到第一个维度的出现。事实上,第一个维度理论上写多少都是可以的,因为编译器并不在这里做检查。

下面的代码是正确的。

void fun(int x[][11]) {    printID(x);}int main() {    int b[10][11];    printID(b);    fun(b);    return 0;}

高维数组和二维数组类似, 除了第一个维度,其余所有的维度在传参时都必须保持一致 ,这一点请注意。


引用

很多C/C++程序员都学习过或将会学习Java语言。在Java语言中,所有的非基本类型只能通过引用访问,没有生命周期概念,所有的对象由JVM管理其销毁。

但是C/C++的引用与Java完全不同。

在C/C++中,引用具有如下四点性质:

  • 引用的行为是给变量改名
  • 引用不占用额外的内存空间(仅从程序员角度来看,是透明的)
  • 引用不影响被引用对象的生命周期
  • 引用 声明即需初始化,而且在使用过程中不可切换所引用的对象。

引用的声明方式只有一种,那就是在最靠近变量名的位置加&号:

int a;int &b = a;

再次提醒:请不要把引用声明时的&与取地址的&混淆,它们的语义是完全不同的。

在理解引用时,不妨认为引用的内部有一个特殊的指针,这个指针从引用创建开始就不能改变,使用引用时只是对这个地址寻址。

阅读下面的代码:

int *a = new int[12]; // 动态申请内存int &b = a[5]; // 声明一个a[5]的引用delete [] a; //释放由new操作申请的内存

这时,b还是存在的,但是b的引用值已经失效。a变成一个野指针,b变成一个无效引用。

阅读以下代码:

int a[10][10][10];for (int i = 0; i < 10; i++) {    for (int j = 0; j < 10; j++) {        for (int k = 0; k < 10; k++) {            int &b = a[i][j][k];            //对b做一系列操作        }    }}

在内层循环中使用a[i][j][k]会使得代码量增加,看起来会比较乱。

总之,引用能做的事情指针都能做,但是适当使用引用可以使代码变得比较整洁。


指针的*运算符

请注意,这里的*既不是双目运算中的乘号,也不是在声明指针时的标识符,而是指针特有的 单目运算符(STL中的迭代器也有相应的运算符)。

我之所以要先在上面花一定篇幅讲解引用,是因为经过*运算后的指针将表示一个 引用

阅读下面的代码:

int a = 3;int &b = a;int *c = &a;printf("%d %d %d\n", a, b, *c);b = 5;printf("%d %d %d\n", a, b, *c);

运行一下,得到的输出是

3 3 3
5 5 5

相信只要你理解了引用,那么指针的*运算符可以很快理解。


函数指针的类型

直接使用函数名即可得到该函数对应的指针。

我们先来看一下上面fun函数的指针类型:

void fun(int x[][11]) {}int main() {    printID(fun);}

我们可以得到以下输出:

fun: PFvPA11_iE

现在来解释一下这个输出的意义:
PF是Pointer of Function的含义(即函数指针),v表示返回值类型是voidPA11_i上面已经说过了,代表一个二维数组指针,E是停止标志。

再来一个比较复杂的函数声明:

double func(int a, int b[][18][12], char c, const char *d, ...) {}int main() {    printID(func);}

输出如下:

func: PFdiPA18_A12_icPKczE

PF还是函数指针的意思,d表示返回值类型是doublei表示第一个参数是int类型,PA18_A12_i表示下一个参数是一个三维的int数组,c表示下一个参数类型是charPKcconst char *类型的专用表示方法,z表示这个一个可变参数区(如scanf的类型),E为停止标志。


函数指针的声明和使用

请阅读下面的代码:

void hello(int num) {    printf("Hello, %d!\n", num);}int main() {    void(*Demo)(int) = hello;    Demo(5);}

指针声明通用格式如下:

返回值类型(*指针名)(各个参数类型)

记住这个格式即可,不需深究。

下面讲一下如何给一个函数传入一个函数指针以及强制类型转换。

阅读下面的代码:

void hello(int num) {    printf("Hello, %d!\n", num);}void run1(void(*Demo)(int)) {    Demo(5);}void run2(void *Demo) {    ((void(*)(int))Demo)(6); //转化成原始函数指针}int main() {    run1(hello);    run2((void*)hello); //转化成void*指针}

可以看出,传入过程和声明方式类似,但是强制类型转换看起来很不友好。

顺便说一下:通过函数指针,我们可以很方便地在多个同类型函数之间进行切换。这是面向对象编程所摒弃的一种方法,但是在面向过程编程中还是很重要的。如果既想符合面向对象编程规范,又想在运行时灵活地切换函数,可以参考设计模式中的Strategy模式。



如有错误,希望各位读者批评指正,谢谢!


主目录

1 0
原创粉丝点击